Имеет ли смысл проверять значение 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 и т.д.