我认为初始化std::可选的
与std::nullopt
将与默认构造相同。
它们被描述为相当于在cp偏好,作为形式(1)
然而,Clang和GCC似乎都以不同的方式对待这些玩具示例功能。
#include <optional>
struct Data { char large_data[0x10000]; };
std::optional<Data> nullopt_init()
{
return std::nullopt;
}
std::optional<Data> default_init()
{
return {};
}
编译器资源管理器似乎暗示使用std::nullopt
将简单地设置一个字节的"has_value
"标志,
nullopt_init():
mov BYTE PTR [rdi+65536], 0
mov rax, rdi
ret
而默认构造将值初始化类的每个字节。这在功能上是等效的,但几乎总是更昂贵。
default_init():
sub rsp, 8
mov edx, 65537
xor esi, esi
call memset
add rsp, 8
ret
这是有意的行为吗?什么时候应该优先选择一种形式?
更新:GCC(从v11.1开始)和Clang(从v12.0.1开始)现在可以有效地处理这两种形式。
在这种情况下,{}
调用值初始化。如果可选的
的默认构造函数不是用户提供的(其中“非用户提供的”大致意味着“在类定义中被隐式声明或显式默认”),这会导致整个对象的零初始化。
是否这样做取决于特定std::可选
实现的实现细节。看起来libstdc的可选
的默认构造函数不是用户提供的,但libc的是。
对于gcc,默认初始化的不必要归零
std::optional<Data> default_init() {
std::optional<Data> o;
return o;
}
bug86173,需要在编译器本身中修复。使用相同的libstdc,clang在这里不执行任何memset。
在你的代码中,你实际上是在对对象进行值初始化(通过列表初始化)。看起来std::可选的库实现有两个主要选项:要么它们默认默认构造函数(写=default;
,一个基类负责初始化没有值的标志),比如libstdc,要么它们定义默认构造函数,比如libc。
现在在大多数情况下,默认构造函数是正确的做法,它在可能的情况下是微不足道的或constexpr或no的,避免在默认初始化中初始化不必要的东西,等等。这恰好是一个奇怪的情况,用户定义的构造函数有一个优势,这要归功于[decl. init]中语言的一个怪癖,并且默认的通常优势都不适用(我们可以显式地指定confaulr和noAbby)。类类型对象的值初始化从零初始化整个对象开始,如果它是非微不足道的,则在运行构造函数之前,除非默认构造函数是用户提供的(或其他一些技术案例)。这似乎是一个不幸的规范,但是在这个时间点修复它(查看子对象以决定零初始化什么?)可能有风险。
从gcc-11开始,libstdc切换到使用定义的构造函数版本,它生成与std::nullopt相同的代码。同时,务实地,使用std::nullopt的构造函数,它不会使代码复杂化似乎是个好主意。
该标准没有说明这两个构造函数的实现。根据[可选. ctor]:
constexpr optional() noexcept;
constexpr optional(nullopt_t) noexcept;
*this
不包含值。T
,这些构造函数应为constexpr
构造函数(9.1.5)。它只是指定了这两个构造函数的签名及其“保证”(又名效果):在这些构造中的任何一个之后,可选
都不包含任何值。没有给出其他保证。
第一个构造函数是否是用户定义的是实现定义的(即取决于编译器)。
如果第一个构造函数是用户定义的,当然可以实现为设置包含
标志。但是非用户定义的构造函数也符合标准(由gcc实现),因为这也将标志零初始化为false
。虽然它确实导致了昂贵的零初始化,但它并没有违反标准指定的“确保”。
谈到现实生活中的使用,好吧,很高兴您深入研究了实现以编写最佳代码。
附带说明一下,标准可能应该指定这两个构造函数的复杂性(即O(1)
或O(sizeof(T))
)