在 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
是否为空是否有意义?
这会发生吗?怎么样?
是的,这实际上是有道理的。假设CreateMyClass
returnsnullptr
。 pMyClass
(use_count
) 的引用计数变为 1
。当pMyClass
将被销毁时,following will happens:
If *this
owns an object and it is the last shared_ptr
owning it, the
object is destroyed through the owned deleter.
因此,如果自定义删除器取消引用由 shared_ptr(您的代码中的 ptr->close()
)持有的指针,那么它应该负责 nullptr 检查。
注意 empty shared_ptr is not the same as null shared_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)
是一个有效的操作。
如果您的自定义删除器无法处理 p
等于 nullptr
,则在 shared_ptr
的构造函数中传递 null 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 现在不可能拥有空对象。
struct deleter {
template<class T>
void operator()(T*) const {
std::cout << "deleter run\n";
}
};
int main() {
std::shared_ptr<int> bob((int*)0, deleter{});
}
这会打印 "deleter run\n"
。删除者确实是运行.
空的概念和拥有空指针的概念是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
但却是 "non-empty"(标准不使用术语 empty 对于 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
中的外部破坏者处理
这对 std::optional
使用 C++17,但替代 optional
可从 boost
和其他地方获得。
原始 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
等
我看到一些代码使用 std::shared_ptr
和自定义删除器来测试 nullptr 的参数,例如,MyClass
有一个 close()
方法并用一些 CreateMyClass
:
auto pMyClass = std::shared_ptr<MyClass>(CreateMyClass(),
[](MyClass* ptr)
{
if(ptr)
ptr->close();
});
在删除器中测试 ptr
是否为空是否有意义?
这会发生吗?怎么样?
是的,这实际上是有道理的。假设CreateMyClass
returnsnullptr
。 pMyClass
(use_count
) 的引用计数变为 1
。当pMyClass
将被销毁时,following will happens:
If
*this
owns an object and it is the lastshared_ptr
owning it, the object is destroyed through the owned deleter.
因此,如果自定义删除器取消引用由 shared_ptr(您的代码中的 ptr->close()
)持有的指针,那么它应该负责 nullptr 检查。
注意 empty shared_ptr is not the same as null shared_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)
是一个有效的操作。
如果您的自定义删除器无法处理 p
等于 nullptr
,则在 shared_ptr
的构造函数中传递 null 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 现在不可能拥有空对象。
struct deleter {
template<class T>
void operator()(T*) const {
std::cout << "deleter run\n";
}
};
int main() {
std::shared_ptr<int> bob((int*)0, deleter{});
}
这会打印 "deleter run\n"
。删除者确实是运行.
空的概念和拥有空指针的概念是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
但却是 "non-empty"(标准不使用术语 empty 对于 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
中的外部破坏者处理
这对 std::optional
使用 C++17,但替代 optional
可从 boost
和其他地方获得。
原始 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
等