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

时间:2011-05-16 00:11:52

标签: c++ visual-studio-2010 c++11 stl unique-ptr

我在标题中有一些代码如下:

#include <memory>

class Thing;

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

如果我在不包含Thing类型定义的cpp中包含此标头,那么这不会在VS2010-SP1下编译:

  

1&gt; C:\ Program Files(x86)\ Microsoft   视觉工作室   10.0 \ VC \ include \ memory(2067):错误C2027:使用未定义类型'Thing'

std::unique_ptr替换为std::shared_ptr并进行编译。

所以,我猜这是当前VS2010 std::unique_ptr的实现,它需要完整的定义,并且完全取决于实现。

或者是吗?它的标准要求中是否有某些内容使std::unique_ptr的实现无法仅使用前向声明?这感觉很奇怪,因为它只应该指向Thing,不是吗?

9 个答案:

答案 0 :(得分:301)

通过here

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_ptrunique_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_ptrshared_ptr的完整类型。

只有当编译器不需要设置对unique_ptr<A>{A*}的调用时,A构造函数才能使用不完整的~unique_ptr<A>()。例如,如果将unique_ptr放在堆上,则可以使用不完整的A。有关此问题的更多详细信息,请参阅BarryTheHatchet's answer here

答案 1 :(得分:41)

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

答案 2 :(得分:13)

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

答案 3 :(得分:5)

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

以下是发生的事情:

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

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

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

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

答案 4 :(得分:2)

为了完整性:

标题: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,这是有道理的。)

答案 5 :(得分:1)

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

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

答案 6 :(得分:0)

简单的答案是只使用shared_ptr。

答案 7 :(得分:-1)

我一直在寻找一种将PIMPL习惯用法与std::unique_ptr一起使用的方法。 This guide是很好的资源。

简而言之,您可以采取以下措施使其正常工作:

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

答案 8 :(得分:-4)

对于我来说

QList<QSharedPointer<ControllerBase>> controllers;

只需包含标题...

#include <QSharedPointer>