【问题标题】:Is std::unique_ptr<T> required to know the full definition of T?是否需要 std::unique_ptr<T> 才能知道 T 的完整定义?
【发布时间】:2011-08-26 02:40:09
【问题描述】:

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

#include <memory>

class Thing;

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

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

1>C:\Program Files (x86)\Microsoft 视觉工作室 10.0\VC\include\memory(2067): 错误 C2027: 使用未定义类型“事物”

std::unique_ptr 替换为std::shared_ptr 即可编译。

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

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

【问题讨论】:

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


【解决方案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

【讨论】:

    【解决方案2】:

    简单的答案就是使用 shared_ptr 代替。

    【讨论】:

      【解决方案3】:

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

      这是发生了什么:

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

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

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

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

      【讨论】:

      • 谢谢!这是对一个非常晦涩难懂的 C++ 怪癖的非常简洁的解释。为我省去了很多麻烦。
      【解决方案4】:

      对于我来说,

      QList<QSharedPointer<ControllerBase>> controllers;
      

      只需包含标题...

      #include <QSharedPointer>
      

      【讨论】:

      • 回答与问题无关且与问题无关。
      【解决方案5】:

      只是为了完整性:

      标题: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 会再次被销毁,这是有道理的。)

      【讨论】:

        【解决方案6】:

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

        【讨论】:

          【解决方案7】:

          来自here

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

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

          class A;
          A* a = ...;
          delete a;
          

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

          在上面的例子中使用auto_ptr&lt;A&gt; 没有帮助。您仍然会得到与使用原始指针相同的未定义行为。

          尽管如此,在某些地方使用不完整的类是非常有用的!这是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&lt;A&gt;() 的调用时,unique_ptr&lt;A&gt;{A*} 构造函数才能摆脱不完整的A。例如,如果您将unique_ptr 放在堆上,则可以使用不完整的A。关于这一点的更多细节可以在BarryTheHatchet's答案here找到。

          【讨论】:

          • 优秀的答案。如果可以的话,我会 +5。我相信我会在我的下一个项目中再次提到这一点,我正在尝试充分利用智能指针。
          • 如果有人能解释这张表的含义,我想它会帮助更多的人
          • 还有一点需要注意:类的构造函数会引用其成员的析构函数(对于抛出异常的情况,需要调用这些析构函数)。因此,虽然 unique_ptr 的析构函数需要一个完整的类型,但在类中拥有用户定义的析构函数是不够的 - 它还需要一个构造函数。
          • @Mehrdad:这个决定是针对我之前的 C++98 做出的。然而,我相信这个决定来自对可实现性和规范难度的关注(即容器的哪些部分需要或不需要完整类型)。即使在今天,拥有自 C++98 以来 15 年的经验,放宽该领域的容器规范并确保您不会禁止重要的实现技术或优化,这将是一项艰巨的任务。我认为可以做到。我知道这将是很多工作。我知道有人在尝试。
          • 因为从上面的 cmets 中看不出来,对于任何遇到这个问题的人,因为他们将 unique_ptr 定义为类的成员变量,只需 显式 声明一个析构函数 (和构造函数)在类声明中(在头文件中)并继续在源文件中定义它们(并将带有指向类的完整声明的头放在源文件中)以防止编译器在头文件中自动内联构造函数或析构函数(这会触发错误)。 stackoverflow.com/a/13414884/368896 也有助于提醒我这一点。
          【解决方案8】:

          在模板实例化时需要完整的事物定义。这就是 pimpl idiom 编译的确切原因。

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

          【讨论】:

            【解决方案9】:

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

            【讨论】:

            • 我认为这是使用默认函数的绝佳机会。实现文件中的MyClass::~MyClass() = default; 似乎不太可能在以后被认为破坏器主体已被擦除而不是故意留空的人无意中删除。
            • @Dennis Zickefoose:不幸的是,OP 使用的是 VC++,而 VC++ 还不支持 defaulted 和 deleted 类成员。
            • +1 了解如何将门移动到 .cpp 文件中。此外,MyClass::~MyClass() = default 似乎没有将其移入 Clang 的实现文件中。 (还没有?)
            • 您还需要将构造函数的实现移动到 CPP 文件中,至少在 VS 2017 上。例如,请参阅此答案:stackoverflow.com/a/27624369/5124002
            猜你喜欢
            • 2015-04-15
            • 2020-04-03
            • 2012-05-12
            • 2017-12-18
            • 1970-01-01
            • 2015-07-23
            • 2017-10-01
            • 2019-11-07
            • 2018-02-02
            相关资源
            最近更新 更多