关于c ++:std :: unique_ptr和std :: shared_ptr之间的破坏行为差异的基本原理是什么?

What is the rationale for the difference in destruction behavior between std::unique_ptr and std::shared_ptr?

来自http://en.cppreference.com/w/cpp/memory/unique_ptr:

If T is derived class (sic) of some base B, then std::unique_ptr is
implicitly convertible to std::unique_ptr. The default deleter of
the resulting std::unique_ptr will use operator delete for B,
leading to undefined behavior unless the destructor of B is virtual.
Note that std::shared_ptr behaves differently: std::shared_ptr will
use the operator delete for the type T and the owned object will be
deleted correctly even if the destructor of B is not virtual.

以上描述的破坏行为差异的基本原理是什么?我最初的猜测是表演?

同样有趣的是,如果B上的析构函数是非虚拟的,并且从std::shared_ptr的上下文中看不到,那么std::shared_ptr如何能够调用T类型的析构函数?


std::shared_ptr已经在一个原始B*上有一堆开销。

一个shared_ptr基本上保持了4个东西。它维护一个指向EDOCX1的指针〔3〕,它维护两个引用计数(一个"硬"引用计数,一个"软"引用计数,一个用于weak_ptr),并维护一个清除功能。

cleanup函数是shared_ptr行为不同的原因。创建shared_ptr时,调用该特定类型的析构函数的函数被创建并存储在由shared_ptr管理的cleanup函数中。

当更改托管类型(B*变为C*时,清除功能将保持不变。

因为shared_ptr需要管理引用计数,所以清除函数存储的额外开销是微乎其微的。

对于unique_ptr来说,这个类几乎和生的B*一样便宜。它保持除B*以外的零状态,其行为(破坏时)归结为if (b) delete b;。(是的,if (b)是多余的,但是优化器可以解决这个问题)。

为了支持从基到基的强制转换和从派生到删除,必须存储额外的状态,记住unique_ptr实际上是一个派生类。这可以是一个指向deleter的存储指针的形式,比如shared_ptr

但是,这将使EDOCX1的大小增加一倍(11),或者要求它将数据存储在堆的某个地方。

决定unique_ptr应该是零开销,因此在调用基的析构函数时,它不支持强制转换到基。

现在,您可以通过简单地添加一个删除程序类型并存储一个知道它正在破坏的对象类型的破坏函数来教会unique_ptr这样做。上面提到的是unique_ptr的默认删除程序,它是无状态的、琐碎的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct deleter {
  void* state;
  void(*f)(void*);
  void operator()(void*)const{if (f) f(state);}
  deleter(deleter const&)=default;
  deleter(deleter&&o):deleter(o) { o.state = nullptr; o.f=nullptr; }
  deleter()=delete;
  template<class T>
  deleter(T*t):
    state(t),
    f([](void*p){delete static_cast<T*>(p);})
  {}
};
template<class T>
using smart_unique_ptr = std::unique_ptr<T, deleter>;

template<class T, class...Args>
smart_unique_ptr<T> make_smart_unique( Args&&... args ) {
  T* t = new T(std::forward<Args>(args)...);
  return { t, t };
}

实况例子中,我生成一个唯一的ptr到派生,将它存储在一个唯一的ptr到base中,然后重置base。派生指针将被删除。

(一个简单的void(*)(void*)删除程序可能会遇到这样的问题:传入的void*在基本情况和派生情况之间的值会有所不同。)

请注意,在不更改删除程序的情况下更改存储在此类unique_ptr中的指针将导致不明智的行为。