【问题标题】:unique_ptr, pimpl/forward declaration and complete definitionunique_ptr、pimpl/forward 声明和完整定义
【发布时间】:2014-11-09 03:10:16
【问题描述】:

我已经检查了herehere 的问题,但仍然无法找出问题所在。

这是调用代码:

#include "lib.h"

using namespace lib;

int
main(const int argc, const char *argv[]) 
{
    return 0;
}

这是库代码:

#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

clang++ 给了我这个错误:

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

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

【问题讨论】:

  • 这似乎与NSDMI有关......
  • 如果你改变 std::unique_ptr m_impl = nullptr; 会发生什么到 std::unique_ptr m_impl?我认为它应该有效?
  • 我认为这里会出现类似的问题:coliru.stacked-crooked.com/a/984df6900bd1ba8b 该标准似乎对 NSDMI 是否必须有效,即使它被忽略也很模糊。
  • @dyp(即使你删除了 foo 的默认 ctor)
  • 如果没有人很快给出一个体面的答案,我会发邮件给 std-discussion。编译器编写者似乎同意这种行为,但我找不到指定的位置或是否指定。

标签: c++ c++11 language-lawyer


【解决方案1】:

Foo_impl 的实现必须在std::unique_ptr&lt;Foo_impl&gt; m_impl = nullptr 中所需的实例化之前完成。

保留声明的类型(但未初始化)将修复错误 (std::unique_ptr&lt;Foo_impl&gt; 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)
{
}

为什么需要完整类型?

通过= nullptr 的实例化使用copy initialisation 并且需要声明构造函数和析构函数(对于unique_ptr&lt;Foo_impl&gt;)。析构函数需要unique_ptr 的deleter 函数,默认情况下,在指向Foo_impl 的指针上调用delete,因此它需要Foo_impl 的析构函数,而Foo_impl 的析构函数未在不完整中声明类型(编译器不知道它的样子)。也请参阅Howard's answer

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

这种情况的另一种选择可能是使用 直接初始化,如下所示;

std::unique_ptr<Foo_impl> m_impl { nullptr };

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

【讨论】:

  • 这是一个正确的答案。类型必须在 unique_ptr 的 instantiation 点已知(或更准确地说,在调用删除器的点,但由于我们缺少显式析构函数,因此在这种情况下是相同的) ,但不是在声明时。通过在标题中执行= nullptr,声明点和实例化点都移动到封闭类型未知的位置。
  • @leemes,=nullptr 需要构造函数和析构函数。析构函数需要清理函数(默认情况下需要 Foo_impl 的析构函数,它不存在于不完整类型中(编译器不知道它的样子)。
  • @Niall 我看不到标准在哪里说= nullptr 立即执行或需要某些东西。在 [class.base.init]/8 中,NSDMI 与初始化相关联。但是恕我直言的措辞表明这种初始化发生在构造函数内部。这是在类定义之外定义的,应该是在 Foo_impl 完成之后。
  • 为了强调我不理解的内容,这是您解释的第一句话:“通过= nullptr 的实例化需要声明构造函数和析构函数(unique_ptr&lt;Foo_impl&gt; 这里)。” => 为什么?我的意思是,为什么不仅仅在Foo的构造函数和析构函数的定义中?
  • @Niall 我们在谈论不同的目的。我特别问标题中的... impl = nullptr 和构造函数定义中的impl(nullptr) 有什么区别?为什么它将要求“移动”到标题中?直观地说,除了构造函数之外,没有人应该关心标题中的...=...。我认为这是 ctor init 列表中...(...) 的语法糖。但显然它的作用不止于此。
【解决方案2】:

声明:

std::unique_ptr<Foo_impl> m_impl = nullptr;

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

std::unique_ptr<Foo_impl> m_impl = std::unique_ptr<Foo_impl>(nullptr);

即它构造一个临时prvalue。这个临时prvalue必须被破坏。并且该析构函数需要查看Foo_impl 的完整类型。即使省略了纯右值和移动构造,编译器也必须表现得“好像”。

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

std::unique_ptr<Foo_impl> m_impl{nullptr};

更新

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

【讨论】:

  • 析构函数应该不是必需的,但至少g++ instantiates it anyway.
  • @Casey:你说的是第二种(直接初始化)形式吗?啊,按照你的链接,我知道你是。谢谢。
  • 不错。终于有深度的解释了。所以直接初始化是(或应该)相当于“旧方式”(即成员初始化列表)?为什么能够直接在类定义中初始化成员的 C++11 功能通常使用复制初始化的代码 sn-ps 来宣传? (这是题外话,我只是想知道......)
  • @leemes:我最好的猜测是我们(社区)仍在学习如何正确使用该功能。
  • 如果Foo的构造通过异常退出,是否也需要dtor? (堆栈展开期间构造的子对象的破坏)
【解决方案3】:

替换

std::unique_ptr<Foo_impl> m_impl = nullptr;

std::unique_ptr<Foo_impl> m_impl;

修复错误。

【讨论】:

  • 啊,我应该发布我的想法作为答案,但我无法验证:) 这应该是解决方案!
  • @5gon12eder Which version of gcc?
  • @dyp g++ (GCC) 4.9.0 20140604(预发布版)
  • 是的,我指的是修复。没有意识到这是模棱两可的。
  • 我在这个答案中遗漏了一个解释:为什么= nullptr 要求类型是完整的? +1 如果你添加它。
【解决方案4】:

N3936 [temp.inst]/2 个状态:

除非类模板或成员模板的成员已被显式实例化或显式特化,否则当在需要成员定义存在的上下文中引用特化时,会隐式实例化该成员的特化;特别是,静态数据成员的初始化(以及任何相关的副作用)不会发生,除非该静态数据成员本身的使用方式要求该静态数据成员的定义存在。

所以这个问题真正归结为关于该成员类型的析构函数,具有非静态数据成员初始值设定项 (NSDMI) 的声明是否构成“需要成员定义存在的上下文”。虽然很明显,立即需要类型构造函数的 声明 来确定 NSDMI 是否属于初始化成员的适当类型,但我会说构造函数的 定义 /destructor 只有封闭类型的构造函数/析构函数才需要,并且实现不符合要求。

也就是说,核心语言小组目前正在审查 NSDMI 的语义存在几个问题:

所以这里出现混乱也就不足为奇了。

【讨论】:

  • 我什至不清楚是否需要适当的 ctor 声明。这就是我的模板化示例想要展示的内容:即使不使用 NSDMI,当前也会发生错误,而且我不清楚它是否应该是错误。
  • @dyp NSDMI 仍然是一个初始化器,尽管它是一个非常特殊的情况。因此,8.5 的所有要求仍然适用,并且必须进行适当的诊断,无论 NSDMI 是否实际用于 ODR。
  • NSDMI 的语义描述(仅)据我在 [class.base.init]/8 中所见,然后引用 8.5。但这似乎参与了构造函数的内部,即 NSDMI 是(== 等效于)mem-initializers 的语法糖。
  • @dyp 8.5/1 说“在 8.5 的其余部分中描述的初始化过程也适用于由其他语法上下文指定的初始化,例如使用参数表达式的函数参数的初始化 (5.2.2)或返回值的初始化 (6.6.3)。”所以它隐含地适用。从 8.5 中排除 NSDMI 需要一个明确的例外。
  • 我并不是说 NSDMI 不遵循 8.5 的规则,我是想说他们根据 [class.base.init]/8 这样做。很明显,初始化是在调用ctor的时候进行的;现在的问题是,什么时候进行检查? 8.5 的规则何时适用?直接指向定义点,还是在 ctor 内部?
猜你喜欢
  • 2016-05-05
  • 2015-05-01
  • 1970-01-01
  • 2012-09-27
  • 1970-01-01
  • 2021-10-21
  • 1970-01-01
  • 1970-01-01
  • 2012-02-19
相关资源
最近更新 更多