Consider the following code:
#include <new>
#include <malloc.h>
#include <stdio.h>
void * operator new(size_t size) {
void *res;
if (size == 1) {
res = NULL;
} else {
res = malloc(size);
}
fprintf(stderr, "%s(%zu) = %p\n", __PRETTY_FUNCTION__, size, res);
if (res == NULL) throw std::bad_alloc();
return res;
}
void * operator new(size_t size, const std::nothrow_t&) {
void *res;
if (size == 1) {
res = NULL;
} else {
res = malloc(size);
}
fprintf(stderr, "%s(%zu) = %p\n", __PRETTY_FUNCTION__, size, res);
return res;
}
void operator delete(void *ptr) {
fprintf(stderr, "%s(%p)\n", __PRETTY_FUNCTION__, ptr);
free(ptr);
}
void operator delete(void *ptr, const std::nothrow_t&) {
fprintf(stderr, "%s(%p)\n", __PRETTY_FUNCTION__, ptr);
free(ptr);
}
class Foo { };
class Bar {
public:
Bar() : ptr(new Foo()) {
fprintf(stderr, "%s: ptr = %p\n", __PRETTY_FUNCTION__, ptr);
}
Bar(const std::nothrow_t&) noexcept : ptr(new(std::nothrow) Foo()) {
fprintf(stderr, "%s: ptr = %p\n", __PRETTY_FUNCTION__, ptr);
}
~Bar() noexcept {
delete ptr;
}
Foo *ptr;
};
class Baz {
public:
Baz() : ptr(new Foo()) {
fprintf(stderr, "%s: ptr = %p\n", __PRETTY_FUNCTION__, ptr);
}
~Baz() {
delete ptr;
}
Foo *ptr;
};
int main() {
Bar *bar = new(std::nothrow) Bar(std::nothrow_t());
if (bar != NULL) {
delete bar;
} else { fprintf(stderr, "bad alloc on Bar(std::nothrow_t())\n"); }
fprintf(stderr, "\n");
try {
bar = new(std::nothrow) Bar();
delete bar;
} catch (std::bad_alloc) { fprintf(stderr, "bad alloc on Bar()\n"); }
fprintf(stderr, "\n");
try {
Baz *baz = new Baz();
delete baz;
} catch (std::bad_alloc) { fprintf(stderr, "bad alloc on Baz()\n"); }
}
This produces the following output:
void* operator new(size_t, const std::nothrow_t&)(8) = 0x1fed010
void* operator new(size_t, const std::nothrow_t&)(1) = (nil)
Bar::Bar(const std::nothrow_t&): ptr = (nil)
void operator delete(void*)((nil))
void operator delete(void*)(0x1fed010)
void* operator new(size_t, const std::nothrow_t&)(8) = 0x1fed010
void* operator new(std::size_t)(1) = (nil)
void operator delete(void*, const std::nothrow_t&)(0x1fed010)
bad alloc on Bar()
void* operator new(std::size_t)(8) = 0x1fed010
void* operator new(std::size_t)(1) = (nil)
void operator delete(void*)(0x1fed010)
bad alloc on Baz()
As you can see allocating the first Bar succeeds despite the allocation of Foo failing. The second allocation of Bar and alloaction of Baz fail properly through the use of std::bad_alloc.
Now my question is: How to make "new(std::nothrow) Bar(std::nothrow_t());" free the memory for Bar and return NULL when Foo fails to allocate? Is dependency inversion the only solution?
Let's suppose you want to be able to have failed construction without exceptions as a general rule.
I will sketch such a system.
this is a traits class that descendes from
true_type
iff your typeT
has a static method that matches the signaturebool T::emplace_create(T*, Args&&...)
.emplace_create
returns false on creation failure. TheT*
must point to an uninitialized chunk of memory with proper alignment andsizeof(T)
or larger.We can now write this:
which is a function that detects if
T
has_creator
, and if so allocates memory, does anemplace_create
, and if it fails it cleans up the memory and returnsnullptr
. Naturally it uses nothrownew
.You now use
create<T>
in place ofnew
everywhere.The big downside is that we don't support inheritance very well. And composition gets tricky: we basically write our constructor in
emplace_create
and have our actual constructor do next to nothing, and inemplace_create
we handle failure cases (like sub objects having a failedcreate<X>
call).We also get next to no help with inheritance. If we want help with inheritance, we can write two different methods -- one for a no-failure initial construction, and the second for failure-prone creation of resources.
I will note that it gets a touch less annoying if you stop storing raw pointers anywhere. If you store things in
std::unique_ptr
everywhere (even to the point of havingcreate<T>
returningstd::unique_ptr<T>
), and throw in a guarded end-of-scope destroyer with abort, and your destructor has to be able to handle "half-constructed" objects.