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;
};
//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
{}
以上的代码能编译,但是,最普通的Widget
用法却会导致编译出错:
#include "widget.h"
Widget w; //错误!
错误信息根据编译器不同会有所不同,但其文本一般会提到一些有关于“把sizeof
或delete
应用到未完成类型上”的信息。对于未完成类型,使用以上操作是禁止的。
在对象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;
};
在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() //析构函数的定义(译者注:这里高亮)
{}
这样就可以了,并且这样增加的代码也最少,但是,如果你想强调编译器生成的析构函数会做正确的事情——你声明Widget
的析构函数的唯一原因是导致它的定义在 Widget 的实现文件中(译者注:指widget.cpp
)生成,你可以使用“= default
”定义析构函数体:
Widget::~Widget() = default; //同上述代码效果一致
同理,对于移动操作也是一样的。
class Widget { //仍然在“widget.h”中
public:
Widget();
~Widget();
Widget(Widget&& rhs) = default; //思路正确,
Widget& operator=(Widget&& rhs) = default; //但代码错误
…
private: //跟之前一样
struct Impl;
std::unique_ptr<Impl> pImpl;
};
这样的做法会导致同样的错误,和之前的声明一个不带析构函数的类的错误一样,并且是因为同样的原因。
编译器生成的移动赋值操作符,在重新赋值之前,需要先销毁指针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;
《Effective Modern C++》
https://blog.csdn.net/weixin_39894233/article/details/111107675