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


248

我在标头中有一些代码,如下所示:

#include <memory>

class Thing;

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

如果我在不包含Thing类型定义的cpp中包含此标头,则无法在VS2010-SP1下编译:

1> C:\ Program Files(x86)\ Microsoft Visual Studio 10.0 \ VC \ include \ memory(2067):错误C2027:使用未定义类型'Thing'

替换std::unique_ptrstd::shared_ptr并编译。

因此,我猜测这是当前VS2010 std::unique_ptr的实现,需要完整的定义,并且完全依赖于实现。

还是?标准要求中是否有某些内容使得的实现无法std::unique_ptr仅与前向声明一起使用?感觉很奇怪,因为它只应持有一个指向的指针Thing,不是吗?


20
关于何时需要使用C ++ 0x智能指针的完整类型以及shared_ptrunique_ptr不需要完整类型的最好解释是Howard Hinnant的“ Incomplete types and / ,末尾的表应该回答您的问题。
James McNellis 2011年

17
感谢詹姆斯的指导。我忘记了放在那张桌子的位置!:-)
霍华德·辛南特


5
@JamesMcNellis到Howard Hinnant网站的链接已关闭。这是它的web.archive.org版本。无论如何,他在下面用相同的内容完美地回答了这个问题:-)
Ela782

Scott Meyers的《有效的现代C ++》第22条给出了另一个很好的解释。
弗雷德·斯科恩

Answers:


328

这里通过。

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();
    // ...
};

shared_ptrunique_ptr在不同的地方需要完整的类型。原因是模糊的,与动态删除器和静态删除器有关。确切原因并不重要。实际上,在大多数代码中,确切地知道需要完整类型的位置对于您而言并不重要。只是代码,如果您弄错了,编译器会告诉您。

但是,如果这是对您有所帮助,这里是其中记录的几位成员表shared_ptr,并unique_ptr相对于完整性要求。如果成员需要完整类型,则条目具有“ C”,否则表条目将填充“ I”。

Complete type requirements for unique_ptr and shared_ptr

                            unique_ptr       shared_ptr
+------------------------+---------------+---------------+
|          P()           |      I        |      I        |
|  default constructor   |               |               |
+------------------------+---------------+---------------+
|      P(const P&)       |     N/A       |      I        |
|    copy constructor    |               |               |
+------------------------+---------------+---------------+
|         P(P&&)         |      I        |      I        |
|    move constructor    |               |               |
+------------------------+---------------+---------------+
|         ~P()           |      C        |      I        |
|       destructor       |               |               |
+------------------------+---------------+---------------+
|         P(A*)          |      I        |      C        |
+------------------------+---------------+---------------+
|  operator=(const P&)   |     N/A       |      I        |
|    copy assignment     |               |               |
+------------------------+---------------+---------------+
|    operator=(P&&)      |      C        |      I        |
|    move assignment     |               |               |
+------------------------+---------------+---------------+
|        reset()         |      C        |      I        |
+------------------------+---------------+---------------+
|       reset(A*)        |      C        |      C        |
+------------------------+---------------+---------------+

任何需要指针转换的操作都需要同时包含unique_ptr和的完整类型shared_ptr

unique_ptr<A>{A*}构造可以逃脱不完整A仅如果不需要编译器建立一个呼叫~unique_ptr<A>()。例如,如果将unique_ptron放在堆上,则可以通过incomplete来解决A。关于这一点的更多详细信息可以在BarryTheHatchet的答案中找到


3
极好的答案。如果可以的话,我会加5。我敢肯定,我将在下一个项目中再次提到这一点,在该项目中,我试图充分利用智能指针。
matthias'1

4
如果可以解释一下表格的含义,我想它将对更多人有所帮助
Ghita 2012年

8
还有一点要注意:类构造函数将引用其成员的析构函数(对于引发异常的情况,需要调用这些析构函数)。因此,尽管unique_ptr的析构函数需要完整的类型,但在类中具有用户定义的析构函数是不够的-它还需要构造函数。
约翰尼斯·绍布

7
@Mehrdad:这个决定是针对C ++ 98的,这是我的时间。但是,我认为该决定来自对可实现性和规范难度的关注(即,容器的哪些部分确切需要或不需要完整类型)。即使是今天,自C ++ 98以来已有15年的经验,放松这方面的容器规范,并确保您不违背重要的实现技术或优化仍然是一项艰巨的任务。我认为可以做到。我知道这将需要大量工作。我知道有人在尝试。
Howard Hinnant 2013年

9
因为从上面的注释中看不出来,所以对于任何有此问题的人,因为他们将a定义unique_ptr为类的成员变量,只需在类声明(在头文件中)中显式声明一个析构函数(和构造函数),然后继续定义它们放在源文件中(并将带有指向类的完整声明的标头放在源文件中),以防止编译器在标头文件中自动内联构造函数或析构函数(这会触发错误)。 stackoverflow.com/a/13414884/368896也有助于提醒我这一点。
Dan Nissenbaum

42

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


5
我认为这是使用默认功能的绝佳机会。MyClass::~MyClass() = default;实现文件中的文件似乎不太可能在以后被某个人认为是删除了销毁器主体而不是故意将其留空而无意间被删除了。
2011年

@Dennis Zickefoose:不幸的是,OP正在使用VC ++,并且VC ++还不支持defaulted和deleted类成员。
ildjarn

6
+1了解如何将门移动到.cpp文件中。而且似乎MyClass::~MyClass() = default也没有将其移入Clang的实现文件中。(还?)
Eonil

您还需要至少在VS 2017上将构造函数的实现移动到CPP文件。例如,请参见以下答案:stackoverflow.com/a/27624369/5124002
jciloa

15

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


8

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

这是发生了什么事:

如果外部类(即MyClass)没有构造函数或析构函数,则编译器将生成默认的构造函数或析构函数。这样做的问题是,编译器实际上是在.hpp文件中插入默认的空构造函数/析构函数。这意味着默认构造函数/析构函数的代码将与主机可执行文件的二进制文件一起编译,而不是与库的二进制文件一起编译。但是,此定义不能真正构造子类。因此,当链接器进入您库的二进制文件并尝试获取构造函数/析构函数时,找不到任何链接,您会收到错误消息。如果构造函数/析构函数代码位于.cpp中,则您的库二进制文件具有可用于链接的代码。

这与使用unique_ptr或shared_ptr无关,其他答案似乎也可能使旧VC ++中的unique_ptr实现混淆(VC ++ 2015在我的机器上工作正常)。

因此,故事的寓意在于,您的标头必须保持不受任何构造函数/析构函数定义的约束。它只能包含其声明。例如,~MyClass()=default;在hpp中将不起作用。如果允许编译器插入默认构造函数或析构函数,则会出现链接器错误。

另一个注意事项:如果即使在cpp文件中有构造函数和析构函数后仍然出现此错误,则最有可能的原因是您的库未正确编译。例如,有一次我只是将项目类型从VC ++中的Console(控制台)更改为Library(库),但由于VC ++未添加_LIB预处理程序符号而产生了完全相同的错误消息,因此出现了此错误。


谢谢!这是一个非常晦涩的C ++怪癖的非常简洁的解释。省了我很多麻烦。
JPNotADragon

4

仅出于完整性考虑:

标头:啊

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的定义。这在考虑到B时是有意义的如果构造函数发生异常,则unique_ptr会再次销毁。)


1

在模板实例化时,需要Thing的完整定义。这就是pimpl习惯用法编译的确切原因。

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



-7

至于我

QList<QSharedPointer<ControllerBase>> controllers;

只包括标题...

#include <QSharedPointer>

答案与问题无关且无关。
Mikus
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.