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

303

我有一些头文件中的代码看起来像这样:

#include <memory>

class Thing;

class MyClass
{
    std::unique_ptr< Thing > my_thing;
};
如果我在一个cpp文件中包含此头文件,但是没有包含Thing类型定义,那么在VS2010-SP1下它将无法编译:

C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include\memory(2067): error C2027: use of undefined type 'Thing'

std::unique_ptr替换为std::shared_ptr便会编译通过。
那么,我猜测当前的VS2010的std::unique_ptr实现需要完整的定义,这完全取决于其实现方式。
或者呢?std::unique_ptr的标准要求中是否有关于它的实现只能使用前向声明而不能正常工作的内容?感觉很奇怪,因为它应该只保持对Thing的指针,不是吗?

29
C++0x智能指针中何时需要完整类型的最佳解释是Howard Hinnant的文章《Incomplete types and shared_ptr/unique_ptr》,链接为http://home.roadrunner.com/~hinnant/incomplete.html。文章末尾的表格可以回答你的问题。 - James McNellis
26
谢谢你的指引,詹姆斯。我忘记了我放那张桌子了!:-) - Howard Hinnant
https://dev59.com/KWox5IYBdhLWcg3wtmg1#49187113 - parasrish
11
@JamesMcNellis,Howard Hinnant的网站链接已经无法访问。这里是web.archive.org的版本。无论如何,他在下面完美地回答了相同的内容 :-) - Ela782
1
另一个很好的解释可以在Scott Meyers的《Effective modern C++》的第22项中找到。 - Fred Schoen
2
http://howardhinnant.github.io/incomplete.html - telephone
9个回答

404

采用自这里

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的几个操作与完整性要求相关的信息。

操作 unique_ptr shared_ptr
P()(默认构造函数) 不完整的 不完整的
P(const P&)(复制构造函数) 不完整的
P(P&&)(移动构造函数) 不完整的 不完整的
~P()(析构函数) 完整的 不完整的
P(A*)(从指针构造函数) 不完整的 完整的
operator=(const P&)(复制赋值运算符) 不完整的
operator=(P&&)(移动赋值运算符) 完整的 不完整的
reset() 完整的 不完整的
reset(A*) 完整的 完整的
任何需要指针转换的操作都需要完整的类型,即对于unique_ptrshared_ptr都是如此。
只有在编译器不需要设置调用~unique_ptr<A>()时,unique_ptr<A>{A*}构造函数才能使用不完整的A。例如,如果将unique_ptr放在堆上,则可以使用不完整的A。关于这一点的更多细节可以在BarryTheHatchet的答案here中找到。

4
非常好的回答。如果我能的话,我会给它加5分。我相信在我的下一个项目中,我会参考这个回答,因为我试图充分利用智能指针。 - matthias
5
如果能够解释这张表的含义,我想会帮助更多的人。 - Ghita
11
额外提示:一个类的构造函数将引用其成员的析构函数(对于抛出异常的情况,这些析构函数需要被调用)。因此,虽然unique_ptr的析构函数需要完整类型,但仅在类中拥有用户定义的析构函数是不够的——它还需要一个构造函数。 - Johannes Schaub - litb
8
@Mehrdad:这个决定是为C++98做出的,那是在我加入之前。不过我相信这个决定是基于可实现性和规范难度的考虑(即容器的哪些部分需要或不需要完全类型)。即使是今天,距离C++98已经有15年的经验,放松容器规范并确保不禁止重要的实现技术或优化仍然是一个非常困难的任务。我认为这可能是可以做到的,但我知道这需要大量的工作。我知道有一个人正在尝试这样做。 - Howard Hinnant
25
如果有人因为将unique_ptr定义为类的成员变量而遇到问题,就需要在类声明中(头文件中)显式地声明一个析构函数(和构造函数),然后在源文件中定义它们(并将带有指向的类完整声明的头文件放在源文件中),以防止编译器自动内联头文件中的构造函数或析构函数(这会触发错误)。https://dev59.com/YWYr5IYBdhLWcg3wxM0k#13414884也提醒了我这一点。 - Dan Nissenbaum
显示剩余19条评论

51
编译器需要Thing的定义才能为MyClass生成默认析构函数。如果你显式声明了析构函数并将其(空)实现移到 CPP 文件中,代码应该可以编译。

11
我认为这是使用默认函数的绝佳机会。在实现文件中使用 MyClass::~MyClass() = default; 似乎不太可能被误删除,因为某些人可能会假设析构函数体已经被删除而不是故意留空。 - Dennis Zickefoose
@Dennis Zickefoose:不幸的是,OP正在使用VC++,而VC++尚不支持defaultdelete类成员。 - ildjarn
6
如何将门移入.cpp文件中,我给予+1。另外,在Clang上似乎MyClass::~MyClass() = default不能将其移入实现文件中。(至少目前还不能) - eonil
你还需要将构造函数的实现移动到 CPP 文件中,至少在 VS 2017 中是这样的。例如,参见此答案:https://dev59.com/vF4c5IYBdhLWcg3w-OM4#27624369 - jciloa

20

这不是依赖于实现的。它能够正常工作的原因是因为 shared_ptr 在运行时确定要调用的正确析构函数,而这并不是类型签名的一部分。然而,unique_ptr 的析构函数 其类型的一部分,必须在编译时知道。


16
看起来当前的答案并没有准确说明为什么默认构造函数(或析构函数)会有问题,但在cpp文件中声明空的默认构造函数(或析构函数)却不会有问题。
这里发生了什么:
如果外部类(例如 MyClass)没有构造函数或析构函数,则编译器会生成默认的构造函数/析构函数。问题在于编译器在 .hpp 文件中实际上插入了默认的空构造函数/析构函数定义。这意味着默认构造函数/析构函数的代码会与主机可执行文件的二进制文件一起编译,而不是与库的二进制文件一起编译。然而,这些定义无法真正构造分部类。因此,当链接器进入您的库的二进制文件并尝试获取构造函数/析构函数时,并未找到任何内容,从而导致错误。如果构造函数/析构函数的代码位于您的 .cpp 中,则库的二进制文件将可用于链接。
这与使用unique_ptr或shared_ptr无关,其他答案似乎可能是旧版VC++中unique_ptr实现的混淆bug(VC++ 2015在我的机器上正常工作)。
所以故事的寓意是,您的头文件需要保持没有任何构造函数/析构函数定义。它只能包含它们的声明。例如,在hpp中, ~MyClass()=default;是无法正常工作的。如果允许编译器插入默认构造函数或析构函数,则会导致链接器错误。
另外,一个副注意点:如果即使在cpp文件中仍然出现此错误,则最可能的原因是您的库没有正确编译。例如,有一次我简单地将项目类型从 Console 改为 Library 在VC++中,我遇到了这个错误,因为VC++没有添加_LIB预处理器符号,这产生了完全相同的错误消息。

谢谢!那是一个非常简洁的解释,针对一个极其晦涩的C++怪癖。省了我很多麻烦。 - JPNotADragon
这真的没有解释另一部分内容:“用std::shared_ptr替换std::unique_ptr,然后它就能编译。”你在回答中讲述的故事的寓意,为什么不适用于shared_ptr呢? - Nitesh

14

仅为完整起见:

头文件: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再次被销毁时,这是有意义的。)


4
我正在寻找一种使用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

1

在模板实例化的时候,需要完整定义 Thing。这正是 pimpl 惯用法能够编译的原因。

如果不可能的话,人们也不会像 this 这样提出问题。


-9
简单的答案就是使用 shared_ptr。

-11

就我而言,

QList<QSharedPointer<ControllerBase>> controllers;

只需包含头文件...

#include <QSharedPointer>

1
答案与问题无关且不相关。 - Mikus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接