在 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 是否为空是否有意义? 这会发生吗?怎么样?

是的,这实际上是有道理的。假设CreateMyClassreturnsnullptrpMyClass (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{});
}

Live example.

这会打印 "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,而不仅仅是持有 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_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 更容易编写,但我不确定我是否做对了。

Live example.

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