【问题标题】:Why std::shared_ptr calls destructors from base and derived classes, where delete calls only destructor from base class? [duplicate]为什么 std::shared_ptr 从基类和派生类调用析构函数,而 delete 仅从基类调用析构函数? [复制]
【发布时间】:2014-01-15 04:05:39
【问题描述】:

为什么当使用 std::shared_ptr 解除分配从基类和派生类调用析构函数时,而第二个示例仅从基类调用析构函数?

class Base
{
public:
    ~Base()
    {
        std::cout << "Base destructor" << std::endl;
    }
};

class Derived : public Base
{
public:
    ~Derived()
    {
        std::cout << "Derived destructor" << std::endl;
    }
};

void virtual_destructor()
{
    {
        std::cout << "--------------------" << std::endl;
        std::shared_ptr<Base> sharedA(new Derived);
    }

    std::cout << "--------------------" << std::endl;
    Base * a = new Derived;
    delete a;
}

输出:

--------------------
Derived destructor
Base destructor
--------------------
Base destructor

我期望在这两种情况下都有相同的行为。

【问题讨论】:

  • 你的基础析构函数一开始就不是虚拟的。你在找麻烦……

标签: c++ shared-ptr


【解决方案1】:

delete a 是未定义的行为,因为类Base 没有虚拟析构函数,并且*a 的“完整对象”(更准确地说:包含*a 的最衍生对象)不是类型Base.

共享指针是使用推导删除器创建的,该删除器删除Derived *,因此一切都很好。

(推导删除器的效果就是说delete static_cast&lt;Derived*&gt;(__the_pointer))。

如果你想用共享指针重现未定义的行为,你必须立即转换指针:

// THIS IS AN ERROR
std::shared_ptr<Base> shared(static_cast<Base*>(new Derived));

从某种意义上说,这是共享指针行为的正确方式:因为您已经为类型擦除的删除器和分配器的虚拟查找付出了代价,所以这是公平的那么您不必为析构函数的另一个虚拟查找付费。类型擦除删除器会记住完整的类型,因此不会产生更多开销。

【讨论】:

  • 公平的是,您不必再为析构函数的另一个虚拟查找付费 shared_ptr 的实现如果析构函数是虚拟的,则支付动态调度的成本。成本是不是这样做的原因,如果删除器没有编码原始指针的类型(可能是也可能不是完整指针的类型),共享指针的其他功能将不起作用对象),例如给子对象起别名,或维护具有不同类型(基类与派生类)的共享指针。最后一段充满了问题。
  • 顺便说一句,在删除器中禁用虚拟调度会导致未定义的行为,例如:struct base { virtual ~base(); }; struct derived : base {}; base *p = new derived; std::shared_ptr&lt;base&gt; sp(p);,这可能是一个常见的用例(指向接口的指针被传递给管理它的库内部通过共享指针)
  • @DavidRodríguez-dribeas:也许我并没有明确说明删除器不会禁用虚拟调度(无论如何,对于删除表达式来说,这是不可能的),但是如果您只在共享指针中使用层次结构,它可以让您选择不需要虚拟析构函数。
  • 但是对删除器删除函数的调用是虚拟的。因为它不能作为其具体类型存储到类型擦除的共享指针中。因此,我们在销毁时为任何类型支付虚拟调用。以及带有虚拟析构函数的类型的双重虚拟间接。一个用于删除操作符 ()(或 Matthieu 代码中的 destruct() 函数),另一个用于 delete 操作符。
  • @v.oddou:是的,确实,shared_ptr 本身总是为类型擦除付出代价。
【解决方案2】:

Kerrek SB 的回答中缺少的部分是shared_ptr 是如何知道类型的

答案是涉及3种类型:

  • 指针的静态类型 (shared_ptr&lt;Base&gt;)
  • 传递给构造函数的静态类型
  • 数据的实际动态类型

shared_ptr 不知道实际的动态类型,但知道传递给其构造函数的静态类型。然后它会进行类型擦除......但记住以某种方式。一个示例实现是(不共享):

template <typename T>
class simple_ptr_internal_interface {
public:
    virtual T* get() = 0;
    virtual void destruct() = 0;
}; // class simple_ptr_internal_interface

template <typename T, typename D>
class simple_ptr_internal: public simple_ptr_internal_interface {
public:
    simple_ptr_internal(T* p, D d): pointer(p), deleter(std::move(d)) {}

    virtual T* get() override { return pointer; }
    virtual void destruct() override { deleter(pointer); }

private:
    T* pointer;
    D deleter;
}; // class simple_ptr_internal

template <typename T>
class simple_ptr {
    template <typename U>
    struct DefaultDeleter {
        void operator()(T* t) { delete static_cast<U*>(t); }
    };

    template <typename Derived>
    using DefaultInternal = simple_ptr_internal<T, DefaultDeleter<Derived>>;

public:
    template <typename Derived>
    simple_ptr(Derived* d): internal(new DefaultInternal<Derived>{d}) {}

    ~simple_ptr() { this->destruct(); }

private:
    void destruct() { internal->destruct(); }

    simple_ptr_internal_interface* internal;
}; // class simple_ptr

请注意,由于这种机制,shared_ptr&lt;void&gt; 实际上是有意义的,可用于携带任何数据并妥善处理。

另请注意,此语义涉及一个惩罚:deleter 属性的类型擦除需要 间接

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-07-18
    • 1970-01-01
    • 2011-05-03
    • 2022-07-01
    • 2013-01-28
    • 2013-06-23
    • 2015-02-24
    • 2011-09-27
    相关资源
    最近更新 更多