smartPtr内存模型文章中,介绍了std::shared_ptrstd::unique_ptr的内存模型区别,这篇文章介绍它们的构造和析构的行为区别。

注意:本文只讨论使用默认删除器的情况,如果使用自定义删除器,则跟删除器的实现有关系,本文讨论的前提和结论可能都不再成立!

参考文章

  1. https://en.cppreference.com/w/cpp/memory/unique_ptr

std::unique_ptr may be constructed for an incomplete type T, such as to facilitate the use as a handle in the pImpl idiom. If the default deleter is used, T must be complete at the point in code where the deleter is invoked, which happens in the destructor, move assignment operator, and reset member function of std::unique_ptr. (Conversely, std::shared_ptr can’t be constructed from a raw pointer to incomplete type, but can be destroyed where T is incomplete).

If T is a derived class 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.

模板参数类型的完整性

  • std::shared_ptr构造时要求模板参数类型是complete type,析构时没有此要求
    • 因为在std::shared_ptr构造的过程中需要创建Control Block,而在Control Block中需要存储被管理类型的析构函数
    • 而在析构时,无论当前的shared_ptr存储的类型是否complete,它指向的Control Block肯定是有完整的deleter的,因为这是在shared_ptr构造时就保证的
  • std::unique_ptr构造时对模板参数类型没有要求,析构时要求必须是complete type
    • unique_ptr没有Control Block,无需在构造时存储管理类型的deleter
    • 而在析构时,unique_ptr需要根据当前类型来调用管理类型的析构函数来释放资源

下面这个例子从侧面证明这一点:

#include <memory>
#include <iostream>

class Incomplete;

int main() {
    Incomplete *raw;
    /**
    In file included from /usr/include/c++/11/bits/shared_ptr.h:53,
                     from /usr/include/c++/11/memory:77,
                     from incomplete_type.cpp:1:
    /usr/include/c++/11/bits/shared_ptr_base.h: In instantiation of ‘std::__shared_ptr<_Tp, _Lp>::__shared_ptr(_Yp*) [with _Yp = Incomplete; <template-parameter-2-2> = void; _Tp = Incomplete; __gnu_cxx::_Lock_policy _Lp = __gnu_cxx::_S_atomic]’:
    /usr/include/c++/11/bits/shared_ptr.h:160:46:   required from ‘std::shared_ptr<_Tp>::shared_ptr(_Yp*) [with _Yp = Incomplete; <template-parameter-2-2> = void; _Tp = Incomplete]’
    incomplete_type.cpp:8:46:   required from here
    /usr/include/c++/11/bits/shared_ptr_base.h:1102:26: error: invalid application of ‘sizeof’ to incomplete type ‘Incomplete’
     1102 |           static_assert( sizeof(_Yp) > 0, "incomplete type" );
          |                          ^~~~~~~~~~~
    In file included from /usr/include/c++/11/memory:76,
                     from incomplete_type.cpp:1:
    /usr/include/c++/11/bits/unique_ptr.h: In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Incomplete]’:
    /usr/include/c++/11/bits/unique_ptr.h:361:17:   required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Incomplete; _Dp = std::default_delete<Incomplete>]’
    incomplete_type.cpp:9:47:   required from here
    /usr/include/c++/11/bits/unique_ptr.h:83:23: error: invalid application of ‘sizeof’ to incomplete type ‘Incomplete’
       83 |         static_assert(sizeof(_Tp)>0,
          |                       ^~~~~~~~~~~
    */

    /**
     * The following code will not compile because the type Incomplete is incomplete.
     * But std::shared_ptr and std::unique_ptr fail for different reasons.
     * As for std::shared_ptr, it requires the complete type of the template argument in constructor.
     *
     * As for std::unique_ptr, it requires the complete type of the template argument in destructor.
     */
    std::shared_ptr<Incomplete> share_ptr(raw);
    std::unique_ptr<Incomplete> unique_ptr(raw);
}

pimpl设计模式unique_ptr incomplete type问题,对std::unique_ptrincomplete type问题做了全面的分析。

析构行为

  • shared_ptr无论如何都会正确的调用其管理对象的析构函数
  • unique_ptr不保证能正确的析构
    • 只有在被管理对象有继承关系,且析构是virtual时才能保证

如下的例子说明这一点:

#include <memory>
#include <iostream>

struct A {

    ~A() {
        std::cout << "A destructor\n";
    }
};

struct B : public A {

    ~B() {
        std::cout << "B destructor\n";
    }
};

struct C {

    virtual ~C() {
        std::cout << "C destructor\n";
    }
};

struct D : public C {

    ~D() override {
        std::cout << "D destructor\n";
    }
};

int main() {
    {
        std::cout << "When desctructor is non-virtual, std::shared_ptr will call correct destructor\n";
        std::shared_ptr<A> a = std::make_shared<B>();
    }
    std::cout << "----" << std::endl;
    {
        std::cout << "When destructor is virtual, std::shared_ptr will call correct destructor\n";
        std::shared_ptr<C> b = std::make_shared<D>();
    }
    std::cout << "----" << std::endl;
    {
        std::cout << "When destructor is non-virtual, std::unique_ptr will NOT call correct destructor\n";
        /// This is undefined behavior, since B is destructed by A's destructor
        std::unique_ptr<A> c = std::make_unique<B>();
    }
    std::cout << "----" << std::endl;
    {
        std::cout << "When destructor is virtual, std::unique_ptr will call correct destructor\n";
        /// Since destructor is virtual, D's destructor will be called
        std::unique_ptr<C> d = std::make_unique<D>();
    }
}