std:: unique_ptr< T>需要知道T的完整定义?

我在标题中有一些代码,看起来像这样:

#include <memory>


class Thing;


class MyClass
{
std::unique_ptr< Thing > my_thing;
};

如果我在一个不包括Thing类型定义的cpp中包含这个头,那么它不会在VS2010-SP1下编译:

1>C:\程序文件(x86)\微软 Visual Studio 10.0\VC\include\memory(2067):错误C2027:使用未定义的类型'Thing'

std::unique_ptr替换为std::shared_ptr,它将被编译。

所以,我猜是当前VS2010 std::unique_ptr的实现需要完整的定义,而且它完全依赖于实现。

真的是这样吗?它的标准要求中是否有一些东西使得std::unique_ptr的实现不可能只使用前向声明?这感觉很奇怪,因为它应该只保存指向Thing的指针,不是吗?

56755 次浏览

编译器需要Thing的定义来为MyClass生成默认析构函数。如果显式声明析构函数并将其(空)实现移动到CPP文件,则代码应该被编译。

在模板实例化时需要Thing的完整定义。这就是为什么要编译pimpl习语的确切原因。

如果不可能,人们就不会问这样的问题。

这与实现无关。它工作的原因是因为shared_ptr确定了在运行时调用的正确析构函数——它不是类型签名的一部分。然而,unique_ptr的析构函数是其类型的一部分,并且必须在编译时知道它。

采用自在这里

c++标准库中的大多数模板都要求用完整类型进行实例化。然而,shared_ptrunique_ptr部分异常。它们的一些成员(但不是所有成员)可以用不完整类型实例化。这样做的动机是使用智能指针支持诸如pimpl这样的习语,并且不会冒未定义行为的风险。

当你有一个不完整的类型并且你对它调用delete时,未定义的行为会发生:

class A;
A* a = ...;
delete a;

以上是法律法规。它将编译。编译器可能会对上面的代码发出警告,也可能不会。当它执行时,可能会发生不好的事情。如果你很幸运,你的程序会崩溃。然而,更可能的结果是您的程序将无声地泄漏内存,因为~A()将不会被调用。

在上面的例子中使用auto_ptr<A>没有帮助。就像使用原始指针一样,您仍然会得到相同的未定义行为。

然而,在某些地方使用不完整的类是非常有用的!这就是shared_ptrunique_ptr起作用的地方。使用这些智能指针之一可以让您摆脱不完整类型,除非需要使用完整类型。最重要的是,当必须使用完整类型时,如果尝试使用不完整类型的智能指针,则会得到编译时错误。

没有更多未定义的行为

如果您的代码被编译,那么您已经在所有需要的地方使用了完整类型。

class A
{
class impl;
std::unique_ptr<impl> ptr_;  // ok!


public:
A();
~A();
// ...
};

unique_ptrshared_ptr的类型完整性要求

shared_ptrunique_ptr在不同的位置需要一个完整的类型。原因是模糊的,与动态删除器和静态删除器有关。具体原因并不重要。事实上,在大多数代码中,确切地知道在哪里需要一个完整的类型并不重要。只是代码,如果你写错了,编译器会告诉你。

然而,为了对你有帮助,这里有一个表,它记录了shared_ptrunique_ptr关于完整性要求的几个操作。

< span style=" font - family:宋体;"> < / th >操作 < span style=" font - family:宋体;"> unique_ptr < / th > < span style=" font - family:宋体;">要< / th > < span style=" font - family:宋体;">P()(默认构造函数) . > . > < span style=" font - family:宋体;"> < em >不完整的< / em > < / td > < span style=" font - family:宋体;"> < em >不完整的< / em > < / td > < span style=" font - family:宋体;">P(const P&)(复制构造函数) < span style=" font - family:宋体;"> - < / td > < span style=" font - family:宋体;"> < em >不完整的< / em > < / td > < span style=" font - family:宋体;">P(P&&)(移动构造函数) < span style=" font - family:宋体;"> < em >不完整的< / em > < / td > < span style=" font - family:宋体;"> < em >不完整的< / em > < / td > < span style=" font - family:宋体;"道明> > ~P()(破坏者)< / < span style=" font - family:宋体;"< / > < >强完成强> < / td > < span style=" font - family:宋体;"> < em >不完整的< / em > < / td > < span style=" font - family:宋体;">P(A*) (ptr构造函数) . bb0 < span style=" font - family:宋体;"> < em >不完整的< / em > < / td > < span style=" font - family:宋体;"< / > < >强完成强> < / td > < span style=" font - family:宋体;">operator=(const P&) (copy赋值) < span style=" font - family:宋体;"> - < / td > < span style=" font - family:宋体;"> < em >不完整的< / em > < / td > < span style=" font - family:宋体;">operator=(P&&)(移动赋值) < span style=" font - family:宋体;"< / > < >强完成强> < / td > < span style=" font - family:宋体;"> < em >不完整的< / em > < / td > < span style=" font - family:宋体;"> reset() td > < / < span style=" font - family:宋体;"< / > < >强完成强> < / td > < span style=" font - family:宋体;"> < em >不完整的< / em > < / td > < span style=" font - family:宋体;"> reset(A*) td > < / < span style=" font - family:宋体;"< / > < >强完成强> < / td > < span style=" font - family:宋体;"< / > < >强完成强> < / td >

任何需要指针转换的操作都需要unique_ptrshared_ptr的完整类型。

只有当编译器不需要设置对~unique_ptr<A>()的调用时,unique_ptr<A>{A*}构造函数才能使用不完整的A。例如,如果将unique_ptr放在堆上,则可以使用不完整的A。关于这一点的更多细节可以在BarryTheHatchet的 answer 在这里中找到。

看起来目前的答案并没有完全确定为什么默认构造函数(或析构函数)是问题,而在cpp中声明的空构造函数不是问题。

事情是这样的:

如果外部类(即MyClass)没有构造函数或析构函数,则编译器会生成默认的构造函数或析构函数。这样做的问题是编译器实际上是在.hpp文件中插入默认的空构造函数/析构函数。这意味着默认构造函数/析构函数的代码与主机可执行文件的二进制文件一起编译,而不是与库的二进制文件一起编译。然而,这个定义并不能真正地构造部分类。所以当链接器进入你的库的二进制并试图获取构造函数/析构函数时,它什么都找不到,你就会得到错误。如果构造函数/析构函数代码在您的.cpp中,那么您的库二进制文件就可以用于链接。

这与使用unique_ptr或shared_ptr和其他答案似乎是旧vc++的unique_ptr实现中可能令人困惑的错误(vc++ 2015在我的机器上工作良好)。

所以这个故事的寓意是你的头文件需要保持不受任何构造函数/析构函数定义的影响。它只能包含他们的声明。例如,hpp中的~MyClass()=default;将无法工作。如果允许编译器插入默认构造函数或析构函数,则会得到链接器错误。

另一个旁注:如果在cpp文件中有构造函数和析构函数后仍然出现这个错误,那么很可能是库没有正确编译。例如,有一次我在vc++中简单地将项目类型从控制台更改为库,我得到了这个错误,因为vc++没有添加_LIB预处理器符号,这产生了完全相同的错误消息。

为了完整起见:

标题:A.h

class B; // forward declaration


class A
{
std::unique_ptr<B> ptr_;  // ok!
public:
A();
~A();
// ...
};

源A.cpp:

class B {  ...  }; // class definition


A::A() { ... }
A::~A() { ... }
类B的定义必须被构造函数、析构函数和任何可能隐式删除B的函数看到。 (虽然构造函数没有出现在上面的列表中,但在VS2017中,即使构造函数也需要定义b。当考虑到构造函数中出现异常时,unique_ptr将再次被销毁时,这是有意义的。

对我来说,

QList<QSharedPointer<ControllerBase>> controllers;

只包括标题…

#include <QSharedPointer>

简单的答案是使用shared_ptr代替。

我正在寻找一种方法来使用PIMPL习语与std::unique_ptr本指南是一个很好的资源。

简而言之,以下是你可以做的事情:

my_class.h

#include <memory>


class Thing;


class MyClass
{
~MyClass(); // <--- Added
std::unique_ptr< Thing > my_thing;
};

my_class.cpp

MyClass::~MyClass() = default; // Or a custom implementation