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

Bra*_*cer 14 c++ libstdc++ language-lawyer clang++ c++17

考虑以下结构,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);
}
Run Code Online (Sandbox Code Playgroud)

使用 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;
                                     ^
Run Code Online (Sandbox Code Playgroud)

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

  • 删除Foo() = default;构造函数会产生相同的结果。
  • Foo() {}作品代替!
  • 删除所有构造函数并初始化fooFoo foo{};工作!
  • 显式地将成员初始化为const std::optional<std::string> m_value{};作品!
  • const从成员中删除!(但意思不一样)
  • -stdlib=libc++作品中使用 clang 9 !
  • 使用 gcc 8.3(仍然使用libstdc++)有效!

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

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

我在编译器资源管理器上看到相同的行为: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.
Run Code Online (Sandbox Code Playgroud)

这是一个错误libstdc++吗?在 clang 和 之间是混合的意图和假设libstdc++吗?

当然,意图不可能是我可以拥有一个带有 a 的结构,const std::stringconst std::optional<std::string>除非我编写了一个构造函数,否则我不能拥有一个带有 a的结构?

(在实际情况下,你也会有额外的构造函数。因此= default()首先构造构造函数的动机。那,和叮当声。)

编辑:这是示例(编译器资源管理器)的扩展版本,显示了一个类似的示例在“pure clang”、“pure 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;
}
Run Code Online (Sandbox Code Playgroud)

eca*_*mur 2

这是以下内容的组合:

  • 标准中的规格不足;
  • 库实现不理想;和
  • 编译器错误。

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

constexpr optional() noexcept = default;
                              ^^^^^^^^^ OK?
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

因此,对于库来说,将默认构造函数定义optional为 defaulted 是一个坏主意,不仅是因为这个原因,还因为它使值初始化optional<B> b{};执行了不必要的工作,因为它必须为零初始化buf,正如观察到的std::可选 - 使用 {} 或 std::nullopt 构造空?- 特别参见这个答案libstdc++ 已在此提交中修复,它将包含在 gcc 的下一个版本(假定为 gcc 11)中。

最后,gcc 中的一个错误是,它允许const非静态数据成员使用非 const-default-constructible 类型,而没有默认构造函数定义为 defaulted 的类类型的默认成员初始值设定项;clang 拒绝它是正确的。减少的测试用例是:

struct S {
    S() = default;
    int const i;
};
Run Code Online (Sandbox Code Playgroud)

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

const std::optional<std::string> m_value = std::nullopt;
                                         ^^^^^^^^^^^^^^
Run Code Online (Sandbox Code Playgroud)

或者(虽然我更喜欢前者,因为它在 Clang/libstdc++ 下提供了更好的代码生成):

const std::optional<std::string> m_value = {};
                                         ^^^^
Run Code Online (Sandbox Code Playgroud)

您还可以考虑提供Foo用户定义的默认构造函数;由于可能是相关的编译器错误,这会在 gcc 下产生更好的代码生成(不将缓冲区清零,而仅将engaged成员设置为)。false