为什么 clang 使用 libstdc++ 删除包含 std::optional 的类型上的显式默认构造函数?

Why does clang, using libstdc++, delete the explicitly defaulted constructor on a type containing std::optional?

考虑以下结构,其中 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; 也存在硬错误,因为它使用了上述已删除的构造函数。

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

的问题

(我怀疑 的答案将成为这里故事的一部分。)

我在 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<std::string> 的结构,除非我写了一个构造函数?

(在真实的情况下,你也会有额外的构造函数。因此首先是 = default() 构造函数的动机。那个,而且很整洁。)

编辑:这是该示例的扩展版本 (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;
}

这是以下的组合:

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

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

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

请注意,functions.within.classes 对 copy/move 构造函数、赋值运算符和非虚析构函数的问题做出了肯定回答,但没有提及默认构造函数。

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

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

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

因此,库将 optional 的默认构造函数定义为默认构造函数是一个坏主意,不仅是因为这个原因,而且还因为它使值初始化的 optional<B> b{}; 执行更多工作不必要的,因为它必须零初始化 buf,正如观察到的那样 - see in particular . libstdc++ is fixed in this commit,它将包含在 gcc 的下一个版本中,假定是 gcc 11.

最后,它是 gcc 中的一个错误,它允许 const 非静态数据成员的非 const-default-constructible 类型没有 class 类型的默认成员初始值设定项默认构造函数被定义为默认; 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),这可能是一个相关的编译器错误。