是否需要知道T的完整定义的std :: unique_ptr <T>?
我有一些代码,如下所示:
#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):error C2027:使用未定义的类型'Thing'
用std::unique_ptr
std::shared_ptr
替换std::unique_ptr
并编译。
所以,我猜测这是当前VS2010 std::unique_ptr
的实现,需要完整的定义,而且完全依赖于实现。
还是呢? 有没有什么标准的要求,使std::unique_ptr
的实现不可能只与前向声明一起工作? 这感觉很奇怪,因为它只应该指向Thing
,不是吗?
从这里通过。
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(); // ... };
shared_ptr
和unique_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*}
构造函数只有在编译器不需要设置对~unique_ptr<A>()
的调用时才能使用不完整的A
例如,如果你把unique_ptr
放在堆上,你可以用一个不完整的A
去掉。 关于这一点的更多细节可以在BarryTheHatchet的答案中找到 。
编译器需要Thing的定义来为MyClass生成默认的析构函数。 如果您显式声明析构函数并将其(空)实现移动到CPP文件,则应该编译代码。
这不是实现相关的。 它工作的原因是因为shared_ptr
决定在运行时调用的正确的析构函数 – 它不是类型签名的一部分。 但是, unique_ptr
的析构函数是其类型的一部分,并且在编译时必须知道。
Thing的完整定义在模板实例化的时候是必需的。 这是pimpl习语编译的确切原因。
如果不可能的话,人们不会问这样的问题。
它看起来像当前的答案不完全明白为什么默认的构造函数(或析构函数)是问题,但在cpp声明空的不是。
以下是最新消息:
如果外部类(即MyClass)没有构造函数或析构函数,则编译器会生成默认的类。 与此问题是编译器本质上插入在.hpp文件中的默认空构造/析构函数。 这意味着默认构造函数/析构函数的代码会与主机可执行文件的二进制文件一起编译,而不是与库的二进制文件一起编译。 但是这个定义不能真正构造分部类。 所以当链接器进入你的库的二进制文件,并试图获得构造/析构函数,它没有找到任何,你会得到错误。 如果构造函数/析构函数的代码在你的.cpp中,那么你的库二进制文件就有可用的链接。
因此,只要使用现代编译器(旧的VC ++编译器可能在unique_ptr实现中有一个错误,但VC ++ 2015在我的机器上正常工作),这与上面的场景使用unique_ptr而不是shared_ptr无关。
因此,故事的寓意是你的头文件需要保持没有任何构造/析构函数的定义。 它只能包含他们的声明。 例如, ~MyClass()=default;
在hpp将无法正常工作。 如果你允许编译器插入默认的构造函数或析构函数,你会得到一个链接错误。
另一方面请注意:如果你仍然得到这个错误,即使在cpp文件中有构造函数和析构函数后,最有可能的原因是你的库没有得到正确的编译。 例如,有一次我简单地将项目类型从控制台更改为VC ++库,我得到这个错误,因为VC ++没有添加_LIB预处理器符号,并产生完全相同的错误信息。