Имеет ли смысл проверять значение nullptr в пользовательском удалении shared_ptr?
Я видел код, который использует std::shared_ptr
с пользовательским удалением, который проверяет аргумент для nullptr, например, MyClass
, который имеет метод close()
и построен с некоторым CreateMyClass
:
auto pMyClass = std::shared_ptr<MyClass>(CreateMyClass(),
[](MyClass* ptr)
{
if(ptr)
ptr->close();
});
Имеет ли смысл тестировать ptr
на неопределенность в deleter?
Может ли это случиться? как?
Ответы
Ответ 1
Конструктор 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
, то недопустимо передать null p
в конструкторе shared_ptr
.
Конструктор, который вы предлагаете в качестве примера, может быть лучше представлен, таким образом:
#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 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 не может иметь нулевой объект.
Ответ 2
Да, это имеет смысл на самом деле. Предположим, что CreateMyClass
возвращает nullptr
. Счетчик ссылок pMyClass
(use_count
) становится 1
. Когда pMyClass
будет уничтожен, произойдет следующее:
Если *this
принадлежит объект, и он является последним shared_ptr
, владеющим им, объект уничтожается через принадлежащего ему департаменту.
Поэтому, если пользовательский делектор разыменовывает указатель, который хранится shared_ptr (ptr->close()
в вашем коде), тогда он должен позаботиться о проверке nullptr.
Обратите внимание, что пустой shared_ptr не совпадает с null shared_ptr.
Ответ 3
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"
. Делектор действительно запущен.
Понятие пустого и понятие принадлежности 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
, а не удалить его; удаление происходит с помощью внешнего разрушителя в make_shared
, обертывающего special_destroyed
.
Это использует С++ 17 для std::optional
, но альтернативный optional
доступен из boost
и в другом месте.
Исходное решение С++ 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
, но я не уверен, что все сделало все в порядке.
Живой пример.
Для версии С++ 11 требуется запись вручную make_index_sequence
и т.д.