在shared_ptr的自定义删除器中检查 nullptr 是否有意义?

Does it make sense to check for nullptr in custom deleter of shared_ptr?

本文关键字:检查 nullptr 是否 有意义 删除 shared ptr 自定义      更新时间:2023-10-16

我见过一些代码将std::shared_ptr与自定义删除器一起使用,用于测试 nullptr 的参数,例如,MyClass具有close()方法并使用一些CreateMyClass构造

auto pMyClass = std::shared_ptr<MyClass>(CreateMyClass(), 
[](MyClass* ptr)
{ 
if(ptr) 
ptr->close(); 
});

在删除器中测试ptr的空性是否有意义? 这能发生吗?如何?

构造函数std::shared_ptr<T>::shared_ptr(Y*p)要求delete p是有效的操作。当p等于nullptr时,这是一个有效的操作。

构造函数std::shared_ptr<T>::shared_ptr(Y*p, Del del)要求del(p)是有效的操作。

如果您的自定义删除器无法处理等于nullptrp则在shared_ptr的构造函数中传递空p是无效的。

您作为示例提供的构造函数可以更好地呈现,因此:

#include <memory>
struct MyClass {
void open() {
// note - may throw
};
void close() noexcept {
// pre - is open
}
};
struct Closer
{
void operator()(MyClass* p) const noexcept
{
p->close();
delete p;  // or return to pool, etc
}
};
auto CreateMyClass() -> std::unique_ptr<MyClass, Closer>
{
// first construct with normal deleter
auto p1 = std::make_unique<MyClass>();
// in case this throws an exception.
p1->open();
// now it's open, we need a more comprehensive deleter
auto p = std::unique_ptr<MyClass, Closer> { p1.release(), Closer() };
return p;
}
int main()
{
auto sp = std::shared_ptr<MyClass>(CreateMyClass());
}

请注意,shared_ptr现在无法拥有空对象。

是的,这实际上是有道理的。假设CreateMyClass返回nullptrpMyClass的引用计数(use_count)变为1。当pMyClass被销毁时,将发生以下情况:

如果*this拥有某个对象,并且它是拥有该对象的最后一个shared_ptr,则 对象通过拥有的删除程序销毁。

因此,如果自定义删除器取消引用由shared_ptr持有的指针(ptr->close()在您的代码中),那么它应该负责 nullptr 检查。

请注意,空shared_ptr与空shared_ptr不同。

struct deleter {
template<class T>
void operator()(T*) const {
std::cout << "deleter runn";
}
};
int main() {
std::shared_ptr<int> bob((int*)0, deleter{});
}

活生生的例子。

这将打印"deleter runn". 删除程序确实已运行。

的概念和拥有 nullptr的概念是shared_ptr的不同概念。

bob是非的,但bob.get()==nullptr. 当非时,将调用析构函数。

int main() {
int x;
std::shared_ptr<int> alice( std::shared_ptr<int>{}, &x );
}

alice是空的,但alice.get() != nullptr. 当alice超出范围时,不会运行delete &x(实际上不会运行析构函数)。

仅当从不使用空指针和删除器构造共享指针时,才能避免这种情况。

解决此问题的一种方法是首先使用自定义删除器创建唯一指针。

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));
}

shared_ptr不同,unique_ptr不能持有nullptr但"非空"(标准没有使用术语空来表示unique_ptr,而是谈论.get()==nullptr)。

unique_ptr可以隐式转换为shared_ptr。 如果它有nullptr,则生成的shared_ptr,而不仅仅是持有nullptrunique_ptr的驱逐舰被带到shared_ptr.


所有这些技术的缺点是,shared_ptr引用计数内存块是单独分配给对象的内存块。 两个分配比一个更糟糕。

但是make_shared构造函数不允许你传入自定义删除器。

如果销毁对象无法抛出,则可以使用别名构造函数非常小心:

// 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)};
}

在这里,我们设法使用一个块进行破坏器和引用计数,同时允许可能失败的open操作,并且仅在数据实际存在时才自动closing

注意,驱逐舰应该关闭MyClass,而不是删除它;删除是由包裹special_destroyedmake_shared中的外部驱逐舰处理的。

这使用C++17表示std::optional,但可以从boost和其他地方获得替代optional


原始 C++14 解决方案。 我们创建一个粗略的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()};
}

这可能太过分了。 幸运的是,我们极其有限的optional可以比真正的optional更容易编写,但我不确定我做对了。

活生生的例子。

C++11版本需要手动编写make_index_sequence等。