我认为用std::nullopt
初始化std::optional
与默认构造相同。
They are described as equivalent at cppreference,如晶型(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
的Compiler Explorer seems to imply将简单地设置 * 一个字节 *“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起)now treat both forms efficiently.
4条答案
按热度按时间uurv41yg1#
在这种情况下,
{}
调用值初始化,如果optional
的默认构造函数不是用户提供的(其中“非用户提供”大致表示“在类定义中隐式声明或显式默认”),则导致整个对象的零初始化。是否这样做取决于特定
std::optional
实现的实现细节,看起来libstdc的optional
的默认构造函数不是用户提供的,但libc的构造函数是用户提供的。lx0bsm1f2#
对于gcc,默认初始化不必要的归零
是bug 86173,需要在编译器中进行修复。使用相同的libstdc++,clang在这里不执行任何memset。
在你的代码中,你实际上是在对对象进行值初始化(通过列表初始化),std::optional的库实现有两个主要选项:它们要么默认默认构造函数(写为
=default;
,一个基类负责初始化表示没有值的标志),如libstdc++;要么定义默认构造函数,如libc++。现在,在大多数情况下,默认构造函数是正确的,它是微不足道的或constexpr或noexcept(如果可能),避免在默认初始化中初始化不必要的东西,等等。这是一个奇怪的情况,用户定义的构造函数有优势,这要归功于[decl.init]语言中的一个怪癖。而且违约通常的好处都不适用(我们可以显式指定constexpr和noexcept).类类型的对象的值初始化开始于零初始化整个对象,如果它不是平凡的,则在运行构造函数之前,除非默认的构造函数是用户提供的(或者其他一些技术案例)。这看起来像是一个不幸的规范,但是在这个时候修正它(查看子对象来决定什么要零初始化?)可能是有风险的。
Starting from gcc-11,libstdc++切换到used-defined constructor版本,它生成与std::nullopt相同的代码。同时,从实用Angular 看,使用std::nullopt中的构造函数不会使代码复杂化似乎是个好主意。
xmjla07d3#
标准没有提到这两个构造函数的实现,根据**[optional.ctor]**:
1.* 确保:*
*this
不包含值。1.备注:没有初始化包含的值。对于每个对象类型
T
,这些构造函数应该是constexpr
构造函数(9.1.5)。它只是指定了这两个构造函数的签名和它们的“确保”(也就是效果):在这些构造之后,
optional
不包含任何值。没有其他保证。第一个构造函数是否是用户定义的由实现定义(即取决于编译器)。
如果第一个构造函数是用户定义的,它当然可以通过设置
contains
标志来实现,但是非用户定义的构造函数也符合标准(如gcc所实现的),因为这也会将标志零初始化为false
,虽然它确实会导致代价高昂的零初始化,但它并不违反标准规定的“确保”。当谈到实际使用时,您已经深入研究了实现以便编写最佳代码,这是很好的。
顺便提一句,也许标准应该指定这两个构造函数的复杂性(即
O(1)
或O(sizeof(T))
)1sbrub3j4#
激励性示例
当我写道:
我希望这个可选项已经初始化,并且不包含未初始化的内存。我也不希望堆崩溃是一个结果,因为我希望一切都初始化好。这与
std::optional
的指针语义相比较:如果我写
std::optional<X*>(std::nullopt)
,我也希望是这样,但至少在这里看起来更像是一个模棱两可的情况。原因是内存未初始化
这种行为很可能是故意的。
(Im不是任何委员会的一员,所以最后我不能肯定)
这是主要原因:一个空的大括号init(zero-init)不应该导致未初始化的内存(尽管语言没有强制这作为一个规则)--否则你怎么保证你的程序中没有未初始化的内存呢?
为了完成这项任务,我们经常使用静态分析工具:突出强调基于执行 *cpp核心准则 * 的 *cpp核心检查 *;特别是,有一些指导方针正好涉及到这个问题,如果这不可能,我们的静态分析将失败,否则这个看似简单的情况;或者更糟的是误导。相比之下,基于堆的容器自然没有同样的问题。
未经检查的访问
请记住,访问
std::optional
是unchecked-这会导致错误地访问未初始化内存的情况。为了演示这一点,如果不是这样,那么这可能是堆损坏:然而,在当前的实现中,这是确定性的(主平台上的seg故障/访问冲突)。
然后你可能会问,为什么
std::nullopt
'specialized'构造函数不初始化内存?我真的不知道为什么它没有。虽然我猜它不会是一个问题,如果它这样做。在这种情况下,作为反对括号初始之一,它没有来与同样的期望。微妙的是,你现在有一个选择。