c++ std::可选-使用{}或std::nullopt?构造空

gcmastyq  于 2023-03-14  发布在  其他
关注(0)|答案(4)|浏览(178)

我认为用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::nulloptCompiler 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.

uurv41yg

uurv41yg1#

在这种情况下,{}调用值初始化,如果optional的默认构造函数不是用户提供的(其中“非用户提供”大致表示“在类定义中隐式声明或显式默认”),则导致整个对象的零初始化。
是否这样做取决于特定std::optional实现的实现细节,看起来libstdcoptional的默认构造函数不是用户提供的,但libc的构造函数是用户提供的。

lx0bsm1f

lx0bsm1f2#

对于gcc,默认初始化不必要的归零

std::optional<Data> default_init() {
  std::optional<Data> o;
  return o;
}

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中的构造函数不会使代码复杂化似乎是个好主意。

xmjla07d

xmjla07d3#

标准没有提到这两个构造函数的实现,根据**[optional.ctor]**:

constexpr optional() noexcept;
constexpr optional(nullopt_t) noexcept;

1.* 确保:* *this不包含值。
1.备注:没有初始化包含的值。对于每个对象类型T,这些构造函数应该是constexpr构造函数(9.1.5)。
它只是指定了这两个构造函数的签名和它们的“确保”(也就是效果):在这些构造之后,optional不包含任何值。没有其他保证。
第一个构造函数是否是用户定义的由实现定义(即取决于编译器)。
如果第一个构造函数是用户定义的,它当然可以通过设置contains标志来实现,但是非用户定义的构造函数也符合标准(如gcc所实现的),因为这也会将标志零初始化为false,虽然它确实会导致代价高昂的零初始化,但它并不违反标准规定的“确保”。
当谈到实际使用时,您已经深入研究了实现以便编写最佳代码,这是很好的。
顺便提一句,也许标准应该指定这两个构造函数的复杂性(即O(1)O(sizeof(T))

1sbrub3j

1sbrub3j4#

激励性示例

当我写道:

std::optional<X*> opt{};
(*opt)->f();//expects error here, not UB or heap corruption

我希望这个可选项已经初始化,并且不包含未初始化的内存。我也不希望堆崩溃是一个结果,因为我希望一切都初始化好。这与std::optional的指针语义相比较:

X* ptr{};//ptr is now zero
ptr->f();//deterministic error here, not UB or heap corruption

如果我写std::optional<X*>(std::nullopt),我也希望是这样,但至少在这里看起来更像是一个模棱两可的情况。

原因是内存未初始化

这种行为很可能是故意的。
(Im不是任何委员会的一员,所以最后我不能肯定)

这是主要原因:一个空的大括号init(zero-init)不应该导致未初始化的内存(尽管语言没有强制这作为一个规则)--否则你怎么保证你的程序中没有未初始化的内存呢?

为了完成这项任务,我们经常使用静态分析工具:突出强调基于执行 *cpp核心准则 * 的 *cpp核心检查 *;特别是,有一些指导方针正好涉及到这个问题,如果这不可能,我们的静态分析将失败,否则这个看似简单的情况;或者更糟的是误导。相比之下,基于堆的容器自然没有同样的问题。

未经检查的访问

请记住,访问std::optionalunchecked-这会导致错误地访问未初始化内存的情况。为了演示这一点,如果不是这样,那么这可能是堆损坏:

std::optional<X*> opt{};//lets assume brace-init doesn't zero-initialize the underlying object for a moment (in practice it does)
(*opt)->f();//<- possible heap corruption

然而,在当前的实现中,这是确定性的(主平台上的seg故障/访问冲突)。
然后你可能会问,为什么std::nullopt 'specialized'构造函数不初始化内存?
我真的不知道为什么它没有。虽然我猜它不会是一个问题,如果它这样做。在这种情况下,作为反对括号初始之一,它没有来与同样的期望。微妙的是,你现在有一个选择。

  • 对于那些感兴趣的MSVC做同样的事情。*

相关问题