提问者:小点点

std::可选-构造空与{}或std::nullopt?


我认为初始化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开始)现在可以有效地处理这两种形式。


共3个答案

匿名用户

在这种情况下,{}调用值初始化。如果可选的的默认构造函数不是用户提供的(其中“非用户提供的”大致意味着“在类定义中被隐式声明或显式默认”),这会导致整个对象的零初始化。

是否这样做取决于特定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;
  1. 确保:*this不包含值。
  2. 备注:没有初始化包含的值。对于每个对象类型T,这些构造函数应为constexpr构造函数(9.1.5)。

它只是指定了这两个构造函数的签名及其“保证”(又名效果):在这些构造中的任何一个之后,可选都不包含任何值。没有给出其他保证。

第一个构造函数是否是用户定义的是实现定义的(即取决于编译器)。

如果第一个构造函数是用户定义的,当然可以实现为设置包含标志。但是非用户定义的构造函数也符合标准(由gcc实现),因为这也将标志零初始化为false。虽然它确实导致了昂贵的零初始化,但它并没有违反标准指定的“确保”。

谈到现实生活中的使用,好吧,很高兴您深入研究了实现以编写最佳代码。

附带说明一下,标准可能应该指定这两个构造函数的复杂性(即O(1)O(sizeof(T))