unique_ptr,pimpl/forward声明和完整定义

my_*_*ion 15 c++ language-lawyer c++11

我已经在这里这里检查了问题,但仍然无法弄清楚出了什么问题.

这是调用代码:

#include "lib.h"

using namespace lib;

int
main(const int argc, const char *argv[]) 
{
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这是lib代码:

#ifndef lib_h
#define lib_h

#include <string>
#include <vector>
#include <memory>

namespace lib
{

class Foo_impl;

class Foo
{
    public:
        Foo();
        ~Foo();

    private:
        Foo(const Foo&);
        Foo& operator=(const Foo&);

        std::unique_ptr<Foo_impl> m_impl = nullptr;

        friend class Foo_impl;
};

} // namespace

#endif
Run Code Online (Sandbox Code Playgroud)

clang ++给了我这个错误:

将'sizeof'无效应用于不完整类型'lib :: Foo_impl'
注意:在成员函数'std :: default_delete :: operator()'的实例化中请求

你可以看到我已经特别声明了Foo析构函数.我还缺少什么?

Nia*_*all 10

Foo_impl必须在实例化之前完成实现std::unique_ptr<Foo_impl> m_impl = nullptr.

保留声明的类型(但未初始化)将修复错误(std::unique_ptr<Foo_impl> m_impl;),然后您需要在代码中稍后初始化它.

您看到的错误来自用于测试此技术的技术的实现; 不完整的类型.基本上,sizeof将导致仅向前声明的类型的错误(即在代码/编译中的该点处使用时缺少定义).

这里有一个可能的解决方案;

class Foo_impl;

class Foo
{
  // redacted
  public:
    Foo();
    ~Foo();

  private:
    Foo(const Foo&);
    Foo& operator=(const Foo&);

    std::unique_ptr<Foo_impl> m_impl;// = nullptr;
};

class Foo_impl {
  // ...
};

Foo::Foo() : m_impl(nullptr)
{
}
Run Code Online (Sandbox Code Playgroud)

为什么需要完整的类型?

实例化通过= nullptr使用复制初始化并要求声明(for unique_ptr<Foo_impl>)构造函数和析构函数.析构函数需要删除函数unique_ptr,默认情况下,调用delete指针,Foo_impl因此它需要析构函数Foo_impl,并且析构函数Foo_impl不是以不完整类型声明(编译器不知道它是什么样的).请参阅霍华德对此的回答.

这里的关键是调用delete一个不完整的类型导致未定义的行为(第5.3.5/5节),因此在实现中明确检查unique_ptr.

对于这种情况的另一种替代方案可以是使用如下的直接初始化 ;

std::unique_ptr<Foo_impl> m_impl { nullptr };
Run Code Online (Sandbox Code Playgroud)

关于非静态数据成员初始化器(NSDMI)似乎存在一些争论,以及这是否是需要成员定义存在的上下文,至少对于clang(可能还有gcc),这似乎是这样的上下文.

  • 这是一个正确的答案.类型必须在unique_ptr的_instantiation_中被知道(或者更确切地说,在调用删除器的位置,但是因为我们缺少一个显式的析构函数,在这种情况下它是相同的),但不是在宣言.通过在头文件中执行`= nullptr`,声明和实例化的点都移动到一个封闭类型未知的地方. (2认同)
  • @Niall我们正在谈论交叉目的.我特别想问一下标题中的`... impl = nullptr`和构造函数定义中的`impl(nullptr)`之间的区别是什么?为什么它将需求"移动"到标题中?直觉上,除了构造函数之外的任何人都不应该关注标题中的`... = ...`.我认为这是ctor init列表中`...(...)`的语法糖.但显然它确实不止于此. (2认同)

How*_*ant 9

该声明:

std::unique_ptr<Foo_impl> m_impl = nullptr;
Run Code Online (Sandbox Code Playgroud)

调用复制初始化.它具有与以下相同的语义:

std::unique_ptr<Foo_impl> m_impl = std::unique_ptr<Foo_impl>(nullptr);
Run Code Online (Sandbox Code Playgroud)

即它构造一个临时的prvalue.必须破坏此临时prvalue.而析构函数需要查看完整类型Foo_impl.即使省略了prvalue和move构造,编译器也必须"好像".

您可以改为使用直接初始化,此时unique_ptr将不再需要析构函数:

std::unique_ptr<Foo_impl> m_impl{nullptr};
Run Code Online (Sandbox Code Playgroud)

更新

Casey指出,~unique_ptr()即使是直接初始化形式,gcc-4.9目前也会实例化.但是在我的测试中,clang没有.我不知道其他编译器可能会做什么.我认为 clang在这方面是合规的,至少在最新的核心缺陷报告中考虑到了.

  • 如果 `Foo` 的构造通过异常退出,不是也需要 dtor 吗?(在堆栈展开期间销毁构造的子对象) (2认同)