在shared_ptr的自定义删除器中检查 nullptr 是否有意义?
Does it make sense to check for nullptr in custom deleter of shared_ptr?
我见过一些代码将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)
是有效的操作。
如果您的自定义删除器无法处理等于nullptr
p
则在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
返回nullptr
。pMyClass
的引用计数(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
为空,而不仅仅是持有nullptr
。unique_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_destroyed
的make_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
等。
- valgrind-hellgrind与泄漏检查的结果不同
- C++模板来检查友元函数的存在
- 检查输入是否不是整数或数字
- 试图让变量检查数组中的某些内容
- 如何在自删除后将对象设置为nullptr
- 如何取消对nullptr的屏蔽,返回正确的对象
- C++ - "!pointer"和"pointer == nullptr"的区别?
- 检查类方法中是否(此 == nullptr)
- 检查模板中 nullptr 的函数指针,了解任何类型的可调用对象
- 检查nullptr是否100%保护内存布局不受segfault影响
- 检查C++中是否只有一个字符串变量不是 nullptr
- 如何使用断言来检查weak_ptr是否为 nullptr
- 返回其参数或检查 nullptr 的函数
- 在shared_ptr的自定义删除器中检查 nullptr 是否有意义?
- 调试器在 nullptr 检查期间引发 nullptr 异常
- Nullptr 并检查指针是否指向有效对象
- 在同一if语句中检查nullptr和有效索引
- 检查std::函数是否已分配给nullptr
- 检查PyObject是否是nullptr
- 我应该总是检查nullptr成员指针吗?