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

#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的指针,不是吗?


当前回答

从这里收养的。

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

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

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

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

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

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

没有更多未定义的行为

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

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

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

unique_ptr和shared_ptr的类型完整性要求

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

但是,为了对您有所帮助,这里有一个表,其中记录了shared_ptr和unique_ptr在完整性要求方面的几个操作。

Operation unique_ptr shared_ptr
P() (default constructor) incomplete incomplete
P(const P&) (copy constructor) incomplete
P(P&&) (move constructor) incomplete incomplete
~P() (destructor) complete incomplete
P(A*) (constructor from ptr) incomplete complete
operator=(const P&) (copy assignment) incomplete
operator=(P&&) (move assignment) complete incomplete
reset() complete incomplete
reset(A*) complete complete

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

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

其他回答

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

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

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

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

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

从这里收养的。

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

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

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

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

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

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

没有更多未定义的行为

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

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

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

unique_ptr和shared_ptr的类型完整性要求

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

但是,为了对您有所帮助,这里有一个表,其中记录了shared_ptr和unique_ptr在完整性要求方面的几个操作。

Operation unique_ptr shared_ptr
P() (default constructor) incomplete incomplete
P(const P&) (copy constructor) incomplete
P(P&&) (move constructor) incomplete incomplete
~P() (destructor) complete incomplete
P(A*) (constructor from ptr) incomplete complete
operator=(const P&) (copy assignment) incomplete
operator=(P&&) (move assignment) complete incomplete
reset() complete incomplete
reset(A*) complete complete

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

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