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

Kla*_*aim 236 c++ stl visual-studio-2010 unique-ptr c++11

我在头文件中有一些代码如下:

#include <memory>

class Thing;

class MyClass
{
    std::unique_ptr< Thing > my_thing;
};
Run Code Online (Sandbox Code Playgroud)

如果我有一个CPP这个头不包含的Thing类型定义,那么这并不在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,不应该吗?

How*_*ant 313

这里采用.

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

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

class A;
A* a = ...;
delete a;
Run Code Online (Sandbox Code Playgroud)

以上是法律代码.它会编译.您的编译器可能会或可能不会发出上述代码的警告.当它执行时,可能会发生不好的事情.如果你很幸运,你的程序会崩溃.然而,一个更可能的结果是你的程序会默默地泄漏内存而~A()不会被调用.

使用auto_ptr<A>在上面的例子中并没有帮助.您仍然会获得与使用原始指针相同的未定义行为.

不过,在某些地方使用不完整的课程非常有用!这是在哪里shared_ptrunique_ptr帮助.使用其中一个智能指针可以让你获得一个不完整的类型,除非必须有一个完整的类型.最重要的是,当需要具有完整类型时,如果您尝试在该点尝试使用不完整类型的智能指针,则会出现编译时错误.

没有更多未定义的行为:

如果您的代码编译,那么您已经在所需的任何地方使用了完整的类型.

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

public:
    A();
    ~A();
    // ...
};
Run Code Online (Sandbox Code Playgroud)

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        |
+------------------------+---------------+---------------+
Run Code Online (Sandbox Code Playgroud)

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

unique_ptr<A>{A*}构造可以用一个不完整的脱身A仅如果不需要编译器来建立一个呼叫~unique_ptr<A>().例如,如果你把它unique_ptr放在堆上,你可以逃脱不完整A.关于这一点的更多细节可以在BarryTheHatchet的答案中找到.

  • 因为从上面的注释中并不明显,对于任何有这个问题的人,因为他们将`unique_ptr`定义为类的成员变量,只需*显式*在类声明中声明析构函数(和构造函数)(在头文件中)并继续在源文件中定义*它们(并在源文件中放置带有指向类的完整声明的头),以防止编译器自动内联头文件中的构造函数或析构函数(触发错误)./sf/answers/939041911/也有助于提醒我这一点. (9认同)
  • 还有一点需要注意:类构造函数将引用其成员的析构函数(对于抛出异常的情况,需要调用这些析构函数).因此,虽然unique_ptr的析构函数需要一个完整的类型,但在类中使用用户定义的析构函数是不够的 - 它还需要一个构造函数. (8认同)
  • @Mehrdad:这个决定是为C++ 98做出的,这是在我的时间之前.但是我认为这个决定来自于对可实现性的关注以及规范的难度(即确切地说容器的哪些部分需要或不需要完整的类型).即使在今天,凭借自C++ 98以来15年的经验,放宽这一领域的容器规范并确保您不会取缔重要的实现技术或优化将是一件非常重要的任务.我认为*它可以完成.我知道*这将是很多工作.我知道有一个人在尝试. (7认同)
  • 如果可以解释表格意味着什么,我猜它会帮助更多人 (4认同)
  • 很好的答案.如果可以的话我会+5.我相信我会在下一个项目中回顾这个问题,我正在尝试充分利用智能指针. (3认同)
  • 我在我的类中添加了构造函数,当我使用unique_ptr <MyType>时,它们现在可以使用Mytype的前向声明.谢谢! (2认同)
  • 请注意我自己与此主题相关的痛苦经历:默认初始化唯一指针(即`std :: unique_ptr &lt;Thing&gt; my_thing {};`)也会阻止转发类声明。解决方案很简单:不要默认初始化唯一指针。 (2认同)

Igo*_*nko 41

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

  • +1如何将门移入.cpp文件.似乎`MyClass :: ~MyClass()= default`也没有将它移动到Clang上的实现文件中.(然而?) (6认同)
  • 我认为这是使用默认功能的绝佳机会.`MyClass :: ~MyClass()= default;`实现文件中的`似乎不太可能被后来无意中删除,因为有人假设destuctor主体被删除而不是故意留空. (5认同)

Pup*_*ppy 15

这不依赖于实现.它工作的原因是因为shared_ptr确定在运行时调用正确的析构函数 - 它不是类型签名的一部分.但是,unique_ptr析构函数它的类型的一部分,它必须在编译时知道.


Shi*_*hah 7

看起来当前的答案并没有确切地说明为什么默认构造函数(或析构函数)是问题但是在cpp中声明的空元素不是.

这是最新发生的事情:

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

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

故事的寓意是你的标题需要保持不受任何构造函数/析构函数的定义.它只能包含他们的声明.例如,~MyClass()=default;在hpp中将无法正常工作.如果允许编译器插入默认构造函数或析构函数,则会出现链接器错误.

另一方面注意:如果在cpp文件中有构造函数和析构函数后仍然出现此错误,那么很可能原因是您的库未正确编译.例如,有一次我只是在VC++中将项目类型从Console改为库,我得到了这个错误,因为VC++没有添加_LIB预处理器符号,并且产生完全相同的错误消息.


Joa*_*him 6

只是为了完整性:

题主:啊

class B; // forward declaration

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

来源 A.cpp:

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

A::A() { ... }
A::~A() { ... }
Run Code Online (Sandbox Code Playgroud)

类 B 的定义必须被构造函数、析构函数和任何可能隐式删除 B 的东西看到。(虽然构造函数没有出现在上面的列表中,但在 VS2017 中,甚至构造函数也需要 B 的定义。考虑到这一点,这是有道理的如果构造函数中出现异常,则 unique_ptr 将再次销毁。)