为什么 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;
也存在硬错误,因为它使用了上述已删除的构造函数。
- 删除
Foo() = default;
构造函数会得到相同的结果。
- 将其替换为
Foo() {}
有效!
- 删除所有构造函数并将
foo
初始化为 Foo foo{};
有效!
- 将成员显式初始化为
const std::optional<std::string> m_value{};
有效!
- 从成员中删除
const
有效! (但不是同一个意思)
- 将 clang 9 与
-stdlib=libc++
一起使用有效!
- 使用 gcc 8.3(仍然使用
libstdc++
)有效!
我读过 ,这似乎表明 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
),这可能是一个相关的编译器错误。
考虑以下结构,其中 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<std::string> m_value{};
有效! - 从成员中删除
const
有效! (但不是同一个意思) - 将 clang 9 与
-stdlib=libc++
一起使用有效! - 使用 gcc 8.3(仍然使用
libstdc++
)有效!
我读过 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
,正如观察到的那样
最后,它是 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
),这可能是一个相关的编译器错误。