为什么不完整类型的智能指针数据成员和原始指针数据成员在其父级析构时具有不同的行为?

Why does incomplete type of smart pointer data member and raw pointer data member have different behavior when their parent destruct?

在下面的代码中:

智能指针数据成员pImpl(classImpl)和原始指针pc(classCAT)都是不完整的数据类型,class中没有这两个定义Widget.h

//widget.h

#ifndef W_H_
#define W_H_
#include <memory>

class Widget { 
    public:
        Widget();
        ~Widget() {    //
            delete pc; // I know I should put ~Widget to .cpp
                       // I just want to show the difference in behavior
                       // between raw pointer and smart pointer(both has incomplete type)
                       // when widget object destructs 
        }
    private:
        struct Impl; 
        std::shared_ptr<Impl> pImpl;  // use smart pointer
        struct CAT;
        CAT *pc;  //raw pointer
};
#endif

//widget.cpp

#include "widget.h"

#include <string>
#include <vector>
#include <iostream>
using namespace std;
struct Widget::CAT 
{
    std::string name;
    CAT(){cout<<"CAT"<<endl;}
    ~CAT(){cout<<"~CAT"<<endl;}
};      


struct Widget::Impl {
    std::string name;
    Impl(){cout<<"Impl"<<endl;}
    ~Impl(){cout<<"~Impl"<<endl;}
};


Widget::Widget()  
    : pImpl(std::make_shared<Impl>()),
      pc(new CAT)
{} 

//main.cpp

#include "widget.h"
int main()
{
    Widget w;
}

//输出

Impl

CAT

~Impl

对于原始指针数据成员,它的析构函数在widget对象被析构时不被调用

虽然 shared_ptr 数据成员,其析构函数已被正确调用。

据我了解,在 Widget::~Widget() 中它应该生成一些 自动编码如下:

        ~Widget() {
            delete pc; // wrote by me
            
            // generated by compiler
            delete pImpl->get();
        }

为什么 shared_ptr 数据成员和原始数据成员在 widget 被销毁时有不同的行为?

我在 Linux

中使用 g++4.8.2 测试代码

================================编辑============ =================== 根据答案,原因是因为:

编译器生成的代码是NOT:

        ~Widget() {
            delete pc; // wrote by me
            
            // generated by compiler
            delete pImpl->get();
        }

可能是这样的:

        ~Widget() {
            delete pc; // wrote by me
            
            // generated by compiler
            pimpl.deleter(); //deleter will be initailized while pimpl object is initialized
        }

因为您在头文件中转发声明 "CAT",所以您的数据类型不完整。有了这些信息,编译器就会陷入未定义的行为,并且可能不会调用析构函数。

标准怎么说

if the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined.

在这里你可以找到详细的解释:Why, really, deleting an incomplete type is undefined behaviour? 只需将结构声明移至 class 定义之前即可解决您的问题:

struct CAT
{
    std::string name;
    CAT(){std::cout<<"CAT"<<std::endl;}
    ~CAT(){std::cout<<"~CAT"<<std::endl;}
};

class Widget {
    public:
        Widget();
        ~Widget() {
            delete pc; // I know we should put this code to cpp
                       // I am just want to show the difference behavior
                       // between raw pointer and smart pointer
                       // when widget object destruct
        }
    private:
        struct Impl;
        std::shared_ptr<Impl> pImpl;  // use smart pointer
        CAT *pc;  //raw pointer
};

和输出

Impl
CAT
~CAT
~Impl

前向声明有助于加快编译时间,但在需要有关数据类型的更多信息时可能会导致问题。

但为什么它适用于智能指针? 这里有一个更好的解释:Deletion of pointer to incomplete type and smart pointers

基本上,shared_ptr只需要初始化或重置指针时的声明。这意味着它在声明时不需要完整的类型。

This functionality isn't free: shared_ptr has to create and store a pointer to the deleter functor; typically this is done by storing the deleter as part of the block that stores the strong and weak reference counts or by having a pointer as part of that block that points to the deleter (since you can provide your own deleter).

说明

您正试图删除一个不完整类型的对象,如果被删除的对象类型有一个非平凡的析构函数。

有关此问题的更多信息,请参阅标准中的 [expr.delete] 以及以下 link:

  • whosebug.com - Why, really, deleting an incomplete type is undefined behaviour?

注意Widget::Cat的析构函数是非平凡的,因为它是用户声明的;反过来这意味着它没有被调用。



解决方案

要解决此问题,只需提供 Widget::Cat 的定义,这样当您执行 delete pc.

时,它就不会 不完整

为什么它适用于 shared_ptr

它在使用 shared_ptr 时起作用的原因是 “删除点” 在您实际构建 [ 之前不会发生=58=] 实例(通过 make_shared;即当 Deleter 实际实例化时。

一个shared_ptr<T>是3个半东西。

它是一个指向T的指针,一个Deleter,一个引用计数。也是弱引用计数(也就是一半)

Deleter 告诉 shared_ptr<T> 当引用计数为 0 时要做什么。从某种意义上说,它与指向 T 的指针无关:您可以使用我所说的 "god mode" 共享指针构造函数以完全分离它们——shared_ptr<T>::shared_ptr( shared_ptr<U>, T* )——并从 shared_ptr<U> 获取你的引用计数,从 T*.

获取你的指针

Deleter 的绑定点在构建时:最常见的两种方式是通过shared_ptr<T>::shared_ptr(T*) 或通过make_shared<T>。到那时,当引用计数returns固定为0时会发生什么。

您可以将 shared_ptr<T> 复制到 shared_ptr<Base> 中,然后 Deleter 随之而来。您可以 "god mode" 窃取引用计数,并将指向成员变量的指针作为指向的类型传递给: 并且原始 Deleter 随之而来。

当一个 shared_ptr<T> 达到 0 引用计数时,它 不知道 它会做什么来销毁:删除器是销毁点的任意任务, 施工时决定。

因此,如果 "how to destroy the T" 在创建智能指针的位置可见,则没有问题。相比之下,对 delete ptr; 的调用直接需要 thd "how to destroy T" 在删除点 可见

这就是为什么你会得到不同的行为。