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

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


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


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

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


这与实现无关。它起作用的原因是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的回答中找到。


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

事情是这样的:

If outer class (i.e. MyClass) doesn't have constructor or destructor then compiler generates the default ones. The problem with this is that compiler essentially inserts the default empty constructor/destructor in the .hpp file. This means that the code for default contructor/destructor gets compiled along with host executable's binary, not along with your library's binaries. However this definitions can't really construct the partial classes. So when linker goes in your library's binary and tries to get constructor/destructor, it doesn't find any and you get error. If the constructor/destructor code was in your .cpp then your library binary has that available for linking.

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

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

另一个旁注:如果在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