【问题标题】:Why does clang, using libstdc++, delete the explicitly defaulted constructor on a type containing std::optional?为什么 clang 使用 libstdc++ 删除包含 std::optional 的类型上的显式默认构造函数?
【发布时间】:2020-08-27 20:52:06
【问题描述】:

考虑以下带有std::optional 的结构,其中包含肯定具有“正常”默认构造函数的类型。

#include <optional>
#include <string>

struct Foo
{
    Foo() = default;
    const std::optional<std::string> m_value;
};

bool function()
{
    Foo foo;
    return bool(foo.m_value);
}

使用 clang 9 编译以下代码(使用系统默认的 libstdc++,用于其 gcc 8)会给出意外警告:

<source>:6:5: warning: explicitly defaulted default constructor is implicitly deleted [-Wdefaulted-function-deleted]
    Foo() = default;
    ^
<source>:7:38: note: default constructor of 'Foo' is implicitly deleted because field 'm_value' of const-qualified type 'const std::optional<std::string>' (aka 'const optional<basic_string<char> >') would not be initialized
    const std::optional<std::string> m_value;
                                     ^

Foo foo; 也存在硬错误,因为它使用了已删除的构造函数。

  • 删除Foo() = default; 构造函数会得到相同的结果。
  • 将其替换为 Foo() {} 有效!
  • 删除所有构造函数并将foo 初始化为Foo foo{}; 有效!
  • 将成员显式初始化为const std::optional&lt;std::string&gt; m_value{}; 有效!
  • 从成员中删除const 有效! (但意思不一样)
  • 将 clang 9 与 -stdlib=libc++ 一起使用有效!
  • 使用 gcc 8.3(仍然使用 libstdc++)有效!

我读过std::optional - construct empty with {} or std::nullopt? 这似乎表明= default 构造函数std::optional 的libstdc++ 实现选择很可能是罪魁祸首。但是,在那个问题上,关注的是一种方法与另一种方法的效率问题。在这种情况下,这似乎是一个正确性的问题。

(我怀疑How can std::chrono::duration::duration() be constexpr? 的答案将成为这里故事的一部分。)

我在 Compiler Explorer 上看到了相同的行为:https://gcc.godbolt.org/z/Yj1o5P

比较可选和非可选std::string 的简单结构(在非工作配置中):

struct Foo { const std::optional<std::string> m_value; };
auto f1() { Foo f; return f.m_value; } // Fails: call to implicitly deleted constructor.

struct Bar { const std::string m_value; };
auto f2() { Bar b; return b.m_value; } // Works.

这是libstdc++ 中的错误吗? clang 和libstdc++ 之间的意图和假设是否混合?

当然,我的意图不可能是我可以有一个带有const std::string 的结构,但我不能有一个带有const std::optional&lt;std::string&gt; 的结构,除非我写了一个构造函数?

(在实际情况下,您也会有额外的构造函数。因此,= default() 构造函数的动机首先是。那,并且clang-tidy。)

编辑:这是示例的扩展版本 (Compiler Explorer),显示了在“纯 clang”、“纯 gcc”中工作的类似示例,但在混合“clang+libstdc++”中失败。这个稍微大一点的例子仍然是人为的,但暗示了为什么人们可能想要真正拥有这样一个默认构造函数。

struct Foo
{
    Foo() = default; // Implicitly deleted?!
    explicit Foo(std::string arg) : m_value{std::move(arg)} {}
    const auto& get() const noexcept { return m_value; }
  private:    
    const std::optional<std::string> m_value;
};

// Maybe return an empty or a full Foo.
auto function(bool flag, std::string x)
{
    Foo foo1;
    Foo foo2{x};
    return flag ? foo1 : foo2;
}

【问题讨论】:

  • [dcl.init]/7 可能是罪魁祸首。
  • const int 成员 (godbolt) 存在编译错误,因此问题可能与 = deafult 和 const 成员变量有关。
  • std::optional 绝对与它有关。是因为const。
  • @IlCapitano:const int value; 删除默认构造函数我并不感到惊讶(因为 int 缺少构造函数)。但似乎const optional&lt;T&gt; value; 应该 工作,不管T 是什么,尤其是当const T value; 工作时。
  • @n.'pronouns'm。当const string x 有效但const optional&lt;string&gt; x 无效时,您能帮我理解它与optional 无关吗?以及为什么“纯clang+libc++”有效,“纯gcc+libstdc++”有效,而“混合clang+libstdc++”无效?

标签: c++ c++17 language-lawyer clang++ libstdc++


【解决方案1】:

这是一个组合:

  • 标准中的规格不足;
  • 一个次优的库实现;和
  • 编译器错误。

首先,标准没有指定默认optional.ctor是否允许实现将其定义为默认:

constexpr optional() noexcept = default;
                              ^^^^^^^^^ OK?

请注意,functions.within.classes 肯定地回答了复制/移动构造函数、赋值运算符和非虚拟析构函数的问题,但没有提及默认构造函数。

这很重要,因为它会影响程序的正确性;假设optional 的数据成员近似如下:

template<class T>
class optional {
    alignas(T) byte buf[sizeof(T)]; // no NSDMI
    bool engaged = false;
    // ...
};

那么由于buf 是一个直接的非变体非静态数据成员,缺少默认成员初始值设定项,如果optional 的默认构造函数被定义为默认构造函数,因此不是用户提供的,则optional 不是const-default-constructible 和 optional&lt;A&gt; const a; 格式不正确。

因此,对于库而言,将 optional 的默认构造函数定义为默认值是一个坏主意,不仅因为这个原因,而且因为它使值初始化的 optional&lt;B&gt; b{}; 执行了不必要的工作,因为它必须零初始化buf,如观察到的std::optional - construct empty with {} or std::nullopt? - 特别参见this answer。 libstdc++ 已在this commit 中修复,它将包含在 gcc 的下一个版本中,推测为 gcc 11。

最后,gcc 中的一个错误是它允许 const 非静态数据成员的非 const-default-constructible 类型没有默认构造函数定义为默认的类类型的默认成员初始化器; clang 拒绝它是正确的。 reduced testcase 是:

struct S {
    S() = default;
    int const i;
};

在您的情况下,最好的解决方法是提供 NSDMI:

const std::optional<std::string> m_value = std::nullopt;
                                         ^^^^^^^^^^^^^^

或者(虽然我更喜欢前者,因为它在 Clang/libstdc++ 下 gives better codegen):

const std::optional<std::string> m_value = {};
                                         ^^^^

你也可以考虑给Foo一个用户定义的默认构造函数;这在 gcc 下的 results in better codegen(不将缓冲区归零,而只是将 engaged 成员设置为 false),这可能是一个相关的编译器错误。

【讨论】:

  • 感谢您的详尽回答。这一切都说得通,我喜欢你对解决方法的建议。
猜你喜欢
  • 2019-08-09
  • 2018-09-20
  • 2017-05-07
  • 1970-01-01
  • 2018-07-17
  • 2023-03-15
  • 2018-02-10
  • 2022-01-09
  • 2019-09-16
相关资源
最近更新 更多