std::optional - 用 或 std::nullopt 构造空?

Posted

技术标签:

【中文标题】std::optional - 用 或 std::nullopt 构造空?【英文标题】:std::optional - construct empty with or std::nullopt?std::optional - 用 或 std::nullopt 构造空? 【发布时间】:2019-09-16 20:48:35 【问题描述】:

我认为用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 ;

Compiler Explorer seems to imply 使用std::nullopt 将简单地设置“包含”标志,

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.

【问题讨论】:

您应该在大多数情况下使用std::nullopt,因为它更加明确并减少了混淆,而且正如您所展示的,有时 充当默认初始化而不是您认为的那样。 作为对我自己问题的评论,为了省去研究这个问题的麻烦......无论使用 Clang 还是 GCC,都会生成较慢的表单;无论使用() 进行初始化;无论 DataPlain Old Data 还是具有已定义的默认构造函数。 Here 是一个展示此行为的孤立玩具示例(基于 libstdc++ 实现)。我添加了一些替代构造函数。我不知道为什么 nullopt1nullopt2 行为不同,但这可能不在本问题的范围内。 再想一想,这更像是一个半骗子而不是 this question 的正确骗子。但是 - 由于某种原因,我无法撤消重复标记。 @einpoklum 感谢您留言。这里的问题来自您链接到的所有答案。我能够在这里取消你的旗帜。 【参考方案1】:

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

是否这样做取决于特定std::optional 实现的实现细节。看起来 libstdc++ 的 optional 的默认构造函数不是用户提供的,但 libc++ 的却是。

【讨论】:

“不是用户提供的” 又名 =defaulted 在类正文中(在这种情况下)。 相信需要提供optional的默认构造函数。 cppreference 是否错误 here 描述构造函数 constexpr optional() noexcept; 这并不排除 =default;ing 它的实现。我认为它在符合标准的程序中是不可观察的,所以好像适用。 我可以推断出“什么时候应该首选一种形式而不是另一种形式?”的答案,但您可能愿意提供一个吗?似乎有 several posts here 暗示等价,但似乎并非如此。 “导致整个对象的零初始化。” -- 即使Data 有用户提供的默认构造函数,这也适用吗?【参考方案2】:

对于 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++ 切换到使用定义的构造函数版本,生成与 std::nullopt 相同的代码。同时,务实地,使用 std::nullopt 中的构造函数不会使代码复杂化似乎是一个好主意。

【讨论】:

optional的默认构造函数不可小觑;您需要初始化“有值”标志。【参考方案3】:

标准没有说明这两个构造函数的实现。根据[optional.ctor]

constexpr optional() noexcept;
constexpr optional(nullopt_t) noexcept;
    确保:*this 不包含值。 备注:没有初始化包含的值。对于每个对象类型 T,这些构造函数应为 constexpr 构造函数 (9.1.5)。

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

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

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

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

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

【讨论】:

【参考方案4】:

励志例子

当我写作时:

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&lt;X*&gt;(std::nullopt) 我也希望如此,但至少在这里看起来更加模棱两可。

原因是内存未初始化

这种行为很可能是故意的

(我不是任何委员会的成员,所以最后我不能确定)

这是主要原因:空大括号 init(零初始化)不应导致内存未初始化(尽管语言不强制执行此规则) - 你还能如何保证你的程序中没有未初始化的内存?

对于这项任务,我们经常转向使用静态分析工具:主要是基于执行 cpp 核心指南cpp 核心检查;特别是有一些关于这个问题的指导方针。如果这不可能,那么对于这个看似简单的情况,我们的静态分析就会失败;或者更糟的是误导。相比之下,基于堆的容器自然不会有同样的问题。

未经检查的访问

请记住,访问std::optional未选中 - 这会导致您可能错误地访问该未初始化内存的情况。 只是为了展示这一点,如果不是这种情况,那么这可能是堆损坏:

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

但是,对于当前的实现,这变得确定性(主要平台上的段错误/访问冲突)。


那你可能会问,为什么std::nullopt'specialized'构造函数初始化内存?

我不太确定为什么没有。虽然我想如果确实如此,那将不是问题。在这种情况下,与大括号初始化相反,它没有同样的期望。巧妙地,你现在有了一个选择。

对于那些对 MSVC 感兴趣的人来说也是如此。

【讨论】:

"a guarenteed seg fault" - 不是的。我曾在通过空指针写入不会导致段错误的平台上工作过;它只是设置一个中断向量。 虽然这在实践中可能工作正常,但您的第一个示例 invokes UB as *this 不包含值。此外,您可以拥有与 std::optional&lt;X*&gt; 相同的属性,当它有意义并且您确实需要它时,您可以显式地进行零初始化。 减慢程序速度以便更容易发现错误(是吗?工具在检测未初始化内存的使用方面变得非常好)对于某些调试模式来说是可以的,而不是最大性能模式。

以上是关于std::optional - 用 或 std::nullopt 构造空?的主要内容,如果未能解决你的问题,请参考以下文章

什么时候适合使用 std::optional

std 可选:没有这样的文件或目录

如何在 C++ 中使用 std::optional ?

std::optional<T> 的开销?

std::optional 成员是不是连续存储?

将一个 std::optional 转换为另一个 std::optional