struct deleter {
template<class T>
void operator()(T*) const {
std::cout << "deleter run\n";
}
};
int main() {
std::shared_ptr<int> bob((int*)0, deleter{});
}
Live example.
This prints "deleter run\n"
. The deleter is indeed run.
The concept of empty and the concept of owning a nullptr are distinct concepts for shared_ptr
.
bob
is non-empty, yet bob.get()==nullptr
. When non-empty, the destructor is called.
int main() {
int x;
std::shared_ptr<int> alice( std::shared_ptr<int>{}, &x );
}
alice
is empty, yet alice.get() != nullptr
. When alice
goes out of scope, delete &x
is not run (and in fact no destructor is run).
This can only be avoided if you never construct your shared pointer with a null pointer and a deleter.
One way to approach this is to first create a unique pointer with a custom deleter.
template<class Deleter, class T>
std::unique_ptr<T, Deleter> change_deleter( std::unique_ptr<T> up, Deleter&& deleter={} ) {
return {up.release(), std::forward<Deleter>(deleter)};
}
struct close_and_delete_foo; // closes and deletes a foo
std::unique_ptr<foo, close_and_delete_foo> make_foo() {
auto foo = std::make_unique<foo>();
if (!foo->open()) return {};
return change_deleter<close_and_delete_foo>(std::move(foo));
}
Unlike shared_ptr
, unique_ptr
cannot hold nullptr
yet be "non-empty" (the standard doesn't use the term empty for unique_ptr
, instead it talks about .get()==nullptr
).
unique_ptr
can be implicitly converted to a shared_ptr
. If it has nullptr
, the resulting shared_ptr
is empty, not just holding nullptr
. The destroyer of the unique_ptr
is carried over to the shared_ptr
.
The downside to all of these techniques is that the shared_ptr
reference counting memory block is a separate allocation to the object's memory block. Two allocations is worse than one.
But the make_shared
constructor doesn't let you pass in a custom deleter.
If destroying your object cannot throw, you can use the aliasing constructor to be extremely careful:
// empty base optimization enabled:
template<class T, class D>
struct special_destroyed:D {
std::optional<T> t;
template<class...Ds>
special_destroyed(
Ds&&...ds
):
D(std::forward<Ds>(ds)...)
{}
~special_destroyed() {
if (t)
(*this)(std::addressof(*t));
}
};
std::shared_ptr<MyClass> make_myclass() {
auto r = std::make_shared< special_destroyed<MyClass, CloseMyClass> >();
r->t.emplace();
try {
if (!r->t->open())
return {};
} catch(...) {
r->t = std::nullopt;
throw;
}
return {r, std::addressof(*r.t)};
}
Here we manage to use one block for destroyer and reference counting, while permitting a possibly failing open
operation, and automatically closing
only when the data is actually there.
Note that the destroyer should only close the MyClass
, not delete it; deleting is handled by an outer destroyer in the make_shared
wrapping the special_destroyed
.
This uses C++17 for std::optional
, but alternative optional
s are available from boost
and elsewhere.
A raw C++14 solution. We create a crude optional
:
template<class T, class D>
struct special_delete:D {
using storage = typename std::aligned_storage<sizeof(T), alignof(T)>::type;
storage data;
bool b_created = false;
template<class...Ts>
void emplace(Ts&&...ts) {
::new( (void*)&data ) T(std::forward<Ts>(ts)...);
b_created=true;
}
template<std::size_t...Is, class Tuple>
void emplace_from_tuple( std::index_sequence<Is...>, Tuple&&tup ) {
return emplace( std::get<Is>(std::forward<Tuple>(tup))... );
}
T* get() {
if (b_created)
return reinterpret_cast<T*>(&data);
else
return nullptr;
}
template<class...Ds>
special_delete(Ds&&...ds):D(std::forward<Ds>(ds)...){}
~special_delete() {
if (b_created)
{
(*this)( get() );
get()->~T();
}
}
};
struct do_nothing {
template<class...Ts>
void operator()(Ts&&...)const{}
};
template<class T, class D, class F=do_nothing, class Tuple=std::tuple<>, class...Ds>
std::shared_ptr<T> make_special_delete(
F&& f={},
Tuple&& args=std::tuple<>(),
Ds&&...ds
) {
auto r = std::make_shared<special_delete<T,D>>(std::forward<Ds>(ds)...);
r->emplace_from_tuple(
std::make_index_sequence<
std::tuple_size<std::remove_reference_t<Tuple>>::value
>{},
std::move(args)
);
try {
f(*r->get());
} catch(...) {
r->b_created = false;
r->get()->~T();
throw;
}
return {r, r->get()};
}
This is probably going too far. Luckily our extremely limited optional
can be written easier than a real optional
, but I'm not certain I did it all right.
Live example.
The C++11 version requires manually writing make_index_sequence
etc.