std::optional - 用 {} 或 std::nullopt 构造空?
std::optional - construct empty with {} or std::nullopt?
我认为初始化 std::optional
with std::nullopt
与默认构造相同。
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
虽然默认构造将 value-initialize class 的每个字节。这在功能上是等效的,但几乎总是更昂贵。
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.
在这种情况下,{}
调用值初始化。如果 optional
的默认构造函数不是用户提供的(其中 "not user-provided" 大致表示 "is implicitly declared or explicitly defaulted within the class definition"),则会导致整个对象的零初始化。
是否这样做取决于特定 std::optional
实现的实现细节。看起来 libstdc++ 的 optional
的默认构造函数不是用户提供的,但 libc++ 的是。
励志例子
当我写的时候:
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)
我也希望如此,但至少在这里它看起来更像是一个模棱两可的情况。
原因是内存未初始化
这种行为很可能是故意的。
(我不是任何委员会的成员,所以最后我不能确定)
这是主要原因:一个空的大括号初始化(零初始化)不应该导致未初始化的内存(尽管语言没有强制执行这一规则) - 您还如何保证您的程序中没有未初始化的内存?
对于此任务,我们经常转向使用静态分析工具:突出地 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
然而,在当前的实施中,这已成为确定性的(在主要平台上违反 fault/access 段)。
那你可能会问,为什么std::nullopt
'specialized'构造函数不初始化内存?
我不太确定为什么没有。虽然我想如果确实如此,那将不是问题。在这种情况下,与 brace-init 不同,它没有相同的期望。微妙的是,你现在有了选择。
对于那些感兴趣的人,MSVC 也做同样的事情。
对于 gcc,默认初始化时不必要的清零
std::optional<Data> default_init() {
std::optional<Data> o;
return o;
}
是 bug 86173 并且需要在编译器本身中修复。使用相同的 libstdc++,clang 在这里不执行任何 memset。
在您的代码中,您实际上是在对对象进行值初始化(通过列表初始化)。 std::optional 的库实现似乎有 2 个主要选项:要么默认默认构造函数(写 =default;
,一个基 class 负责初始化标志,表示没有值) ,像 libstdc++,或者它们定义了默认构造函数,像 libc++。
现在在大多数情况下,默认构造函数是正确的做法,在可能的情况下它是微不足道的或 constexpr 或 noexcept ,避免在默认初始化中初始化不必要的东西等。这恰好是一个奇怪的情况,其中用户定义的构造函数有一个优势,这要归功于 [decl.init] 中语言的一个怪癖,以及 none 默认应用的通常优势(我们可以明确指定 constexpr 和 noexcept)。 class 类型的对象的值初始化从零初始化整个对象开始,在 运行 构造函数之前,如果它是非平凡的,除非默认构造函数是用户提供的(或其他一些技术个案)。这似乎是一个不幸的规范,但在这个时间点修复它(查看子对象来决定对什么进行零初始化?)可能会有风险。
Starting from gcc-11,libstdc++切换到使用定义的构造函数版本,生成与std::nullopt相同的代码。同时,从实用的角度来说,使用 std::nullopt 中的构造函数不会使代码复杂化似乎是个好主意。
标准没有说明这两个构造函数的实现。根据[optional.ctor]:
constexpr optional() noexcept;
constexpr optional(nullopt_t) noexcept;
- 确保:
*this
不包含值。
- 备注:没有初始化包含的值。对于每个对象类型
T
这些构造函数应该是 constexpr
构造函数 (9.1.5).
它只是指定了这两个构造函数的签名及其 "Ensures"(又名效果):在任何这些构造之后,optional
不包含任何值。不提供其他保证。
第一个构造函数是否是用户定义的是实现定义的(即取决于编译器)。
如果第一个构造函数是用户自定义的,当然可以通过设置contains
标志来实现。但是非用户定义的构造函数也符合标准(由 gcc 实现),因为这也将标志零初始化为 false
。尽管它确实会导致代价高昂的零初始化,但它并不违反标准规定的 "Ensures"。
就实际使用而言,很高兴您深入研究了实现以编写最佳代码。
作为旁注,标准可能应该指定这两个构造函数的复杂性(即 O(1)
或 O(sizeof(T))
)
我认为初始化 std::optional
with std::nullopt
与默认构造相同。
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
虽然默认构造将 value-initialize class 的每个字节。这在功能上是等效的,但几乎总是更昂贵。
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.
在这种情况下,{}
调用值初始化。如果 optional
的默认构造函数不是用户提供的(其中 "not user-provided" 大致表示 "is implicitly declared or explicitly defaulted within the class definition"),则会导致整个对象的零初始化。
是否这样做取决于特定 std::optional
实现的实现细节。看起来 libstdc++ 的 optional
的默认构造函数不是用户提供的,但 libc++ 的是。
励志例子
当我写的时候:
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)
我也希望如此,但至少在这里它看起来更像是一个模棱两可的情况。
原因是内存未初始化
这种行为很可能是故意的。
(我不是任何委员会的成员,所以最后我不能确定)
这是主要原因:一个空的大括号初始化(零初始化)不应该导致未初始化的内存(尽管语言没有强制执行这一规则) - 您还如何保证您的程序中没有未初始化的内存?
对于此任务,我们经常转向使用静态分析工具:突出地 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
然而,在当前的实施中,这已成为确定性的(在主要平台上违反 fault/access 段)。
那你可能会问,为什么std::nullopt
'specialized'构造函数不初始化内存?
我不太确定为什么没有。虽然我想如果确实如此,那将不是问题。在这种情况下,与 brace-init 不同,它没有相同的期望。微妙的是,你现在有了选择。
对于那些感兴趣的人,MSVC 也做同样的事情。
对于 gcc,默认初始化时不必要的清零
std::optional<Data> default_init() {
std::optional<Data> o;
return o;
}
是 bug 86173 并且需要在编译器本身中修复。使用相同的 libstdc++,clang 在这里不执行任何 memset。
在您的代码中,您实际上是在对对象进行值初始化(通过列表初始化)。 std::optional 的库实现似乎有 2 个主要选项:要么默认默认构造函数(写 =default;
,一个基 class 负责初始化标志,表示没有值) ,像 libstdc++,或者它们定义了默认构造函数,像 libc++。
现在在大多数情况下,默认构造函数是正确的做法,在可能的情况下它是微不足道的或 constexpr 或 noexcept ,避免在默认初始化中初始化不必要的东西等。这恰好是一个奇怪的情况,其中用户定义的构造函数有一个优势,这要归功于 [decl.init] 中语言的一个怪癖,以及 none 默认应用的通常优势(我们可以明确指定 constexpr 和 noexcept)。 class 类型的对象的值初始化从零初始化整个对象开始,在 运行 构造函数之前,如果它是非平凡的,除非默认构造函数是用户提供的(或其他一些技术个案)。这似乎是一个不幸的规范,但在这个时间点修复它(查看子对象来决定对什么进行零初始化?)可能会有风险。
Starting from gcc-11,libstdc++切换到使用定义的构造函数版本,生成与std::nullopt相同的代码。同时,从实用的角度来说,使用 std::nullopt 中的构造函数不会使代码复杂化似乎是个好主意。
标准没有说明这两个构造函数的实现。根据[optional.ctor]:
constexpr optional() noexcept;
constexpr optional(nullopt_t) noexcept;
- 确保:
*this
不包含值。 - 备注:没有初始化包含的值。对于每个对象类型
T
这些构造函数应该是constexpr
构造函数 (9.1.5).
它只是指定了这两个构造函数的签名及其 "Ensures"(又名效果):在任何这些构造之后,optional
不包含任何值。不提供其他保证。
第一个构造函数是否是用户定义的是实现定义的(即取决于编译器)。
如果第一个构造函数是用户自定义的,当然可以通过设置contains
标志来实现。但是非用户定义的构造函数也符合标准(由 gcc 实现),因为这也将标志零初始化为 false
。尽管它确实会导致代价高昂的零初始化,但它并不违反标准规定的 "Ensures"。
就实际使用而言,很高兴您深入研究了实现以编写最佳代码。
作为旁注,标准可能应该指定这两个构造函数的复杂性(即 O(1)
或 O(sizeof(T))
)