• Pimpl 与 unique_ptr 的问题


    报错信息

    In file included from /usr/include/c++/9/memory:80,
    from /home/xxx/Monitor.h:11,
    from /home/xxx/Monitor.cpp:5:
    /usr/include/c++/9/bits/unique_ptr.h: In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = CM::MemoryPrivate]’:
    /usr/include/c++/9/bits/unique_ptr.h:292:17: required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = CM::MemoryPrivate; _Dp = std::default_deleteCM::MemoryPrivate]’
    /home/xxx/MemoryMonitor.h:19:19: required from here
    /usr/include/c++/9/bits/unique_ptr.h:79:16: error: invalid application of ‘sizeof’ to incomplete type ‘CM::MemoryPrivate’
    79 | static_assert(sizeof(_Tp)>0

    问题

    当使用pimpl手法搭配智能指针unique_ptr时,会出现一个容易忽略的问题

    //widget.h
    class Widget {                     
    public:
        Widget();private:
        struct Impl;
        std::unique_ptr<Impl> pImpl;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    //widget.cpp
    #include "widget.h"                 
    #include "gadget.h"
    #include 
    #include 
    
    struct Widget::Impl {
        std::string name;
        std::vector<double> data;
        Gadget g1,g2,g3;
    };
    
    Widget::Widget()                    //根据条款21,通过std::make_unique
    : pImpl(std::make_unique<Impl>())   //来创建std::unique_ptr
    {}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    以上的代码能编译,但是,最普通的Widget用法却会导致编译出错:

    #include "widget.h"
    
    Widget w;                           //错误!
    
    • 1
    • 2
    • 3

    错误信息根据编译器不同会有所不同,但其文本一般会提到一些有关于“把sizeofdelete应用到未完成类型上”的信息。对于未完成类型,使用以上操作是禁止的。

    原因

    在对象w被析构时(例如离开了作用域),问题出现了。在这个时候,它的析构函数被调用。根据编译器自动生成的特殊成员函数的规则(见 Item17),编译器会自动为我们生成一个析构函数。 在这个析构函数里,编译器会插入一些代码来调用类Widget的数据成员pImpl的析构函数。 pImpl是一个std::unique_ptr,也就是说,一个使用默认删除器的std::unique_ptr。 默认删除器是一个函数,它使用delete来销毁内置于std::unique_ptr的原始指针。

    然而,在使用delete之前,通常会使默认删除器使用C++11的特性static_assert来确保原始指针指向的类型不是一个未完成类型。 当编译器为Widget w的析构生成代码时,它会遇到static_assert检查并且失败,这通常是错误信息的来源。 这些错误信息只在对象w销毁的地方出现,因为类Widget的析构函数,正如其他的编译器生成的特殊成员函数一样,是暗含inline属性的。 错误信息自身往往指向对象w被创建的那行,因为这行代码明确地构造了这个对象,导致了后面潜在的析构。

    解决

    为了解决这个问题,你只需要确保在编译器生成销毁std::unique_ptr的代码之前, Widget::Impl已经是一个完成类型(complete type)。 当编译器“看到”它的定义的时候,该类型就成为完成类型了。 但是 Widget::Impl的定义在widget.cpp里。成功编译的关键,就是在widget.cpp文件内,让编译器在“看到” Widget的析构函数实现之前(也即编译器插入的,用来销毁std::unique_ptr这个数据成员的代码的,那个位置),先定义Widget::Impl

    只需要先在widget.h里,只声明类Widget的析构函数,但不要在这里定义它

    class Widget {                  //跟之前一样,在“widget.h”中
    public:
        Widget();
        ~Widget();                  //只有声明语句private:                        //跟之前一样
        struct Impl;
        std::unique_ptr<Impl> pImpl;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    widget.cpp文件中,结构体Widget::Impl被定义之后,再定义析构函数:

    #include "widget.h"                 //跟之前一样,在“widget.cpp”中
    #include "gadget.h"
    #include 
    #include 
    
    struct Widget::Impl {               //跟之前一样,定义Widget::Impl
        std::string name;
        std::vector<double> data;
        Gadget g1,g2,g3;
    }
    
    Widget::Widget()                    //跟之前一样
    : pImpl(std::make_unique<Impl>())
    {}
    
    Widget::~Widget()                   //析构函数的定义(译者注:这里高亮)
    {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这样就可以了,并且这样增加的代码也最少,但是,如果你想强调编译器生成的析构函数会做正确的事情——你声明Widget的析构函数的唯一原因是导致它的定义在 Widget 的实现文件中(译者注:指widget.cpp)生成,你可以使用“= default”定义析构函数体:

    Widget::~Widget() = default;        //同上述代码效果一致
    
    • 1

    by the way

    同理,对于移动操作也是一样的。

    class Widget {                                  //仍然在“widget.h”中
    public:
        Widget();
        ~Widget();
    
        Widget(Widget&& rhs) = default;             //思路正确,
        Widget& operator=(Widget&& rhs) = default;  //但代码错误private:                                        //跟之前一样
        struct Impl;
        std::unique_ptr<Impl> pImpl;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这样的做法会导致同样的错误,和之前的声明一个不带析构函数的类的错误一样,并且是因为同样的原因。

    编译器生成的移动赋值操作符,在重新赋值之前,需要先销毁指针pImpl指向的对象。然而在Widget的头文件里,pImpl指针指向的是一个未完成类型。

    移动构造函数的情况有所不同。 移动构造函数的问题是编译器自动生成的代码里,包含有抛出异常的事件,在这个事件里会生成销毁pImpl的代码。然而,销毁pImpl需要Impl是一个完成类型。

    因为这个问题同上面一致,所以解决方案也一样——把移动操作的定义移动到实现文件里:

    //widget.h
    class Widget {                          
    public:
        Widget();
        ~Widget();
    
        Widget(Widget&& rhs);               //只有声明
        Widget& operator=(Widget&& rhs);private:                                //跟之前一样
        struct Impl;
        std::unique_ptr<Impl> pImpl;
    };
    
    //widget.cpp
    #include                    struct Widget::Impl {};          //跟之前一样
    
    Widget::Widget()                    //跟之前一样
    : pImpl(std::make_unique<Impl>())
    {}
    
    Widget::~Widget() = default;        //跟之前一样
    
    Widget::Widget(Widget&& rhs) = default;             //这里定义
    Widget& Widget::operator=(Widget&& rhs) = default;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    《Effective Modern C++》
    https://blog.csdn.net/weixin_39894233/article/details/111107675

  • 相关阅读:
    UE4中抛体物理模拟UProjectileMovementComponent
    MacOS 环境编译 JVM 源码
    CTF 全讲解:[SWPUCTF 2021 新生赛]Do_you_know_http
    ImportError: DLL load failed with error code -1073741795
    ChatGPT、GPT-4 Turbo接口调用
    [UE][UE5]像素流送,像素流去掉黑边和按钮
    ElasticSearch三种分页对比
    数据结构—前缀树Trie的实现原理以及Java代码的实现
    PLC SSD来了,固态硬盘SSD马上要成白菜价了吗?
    10多家公司的Java开发面试常见问题合集
  • 原文地址:https://blog.csdn.net/no_say_you_know/article/details/127855984