【发布时间】:2021-06-19 04:30:51
【问题描述】:
我正在尝试编写一个函数make_foo,它将“解开”std::optional< foo >,返回包含的值。
该函数假定启用了可选项,因此不对optional 执行任何运行时检查。
下面是我的实现,以及已编译的程序集以供参考。 我有几个关于编译器输出的问题:
-
为什么这会导致代码分支?
optional::operator*提供对包含值的未经检查的访问,所以我不希望看到任何分支。 -
为什么会调用
foo的析构函数?请注意程序集中对on_destroy()的调用。我们如何在不调用析构函数的情况下将包含的值移出可选项?
C++17 源码
#include <optional>
extern void on_destroy();
class foo {
public:
~foo() { on_destroy(); }
};
extern std::optional< foo > foo_factory();
// Pre-condition: Call to foo_factory() will not return nullopt
foo make_foo() {
return *foo_factory();
}
优化的编译器输出(Clang 11)
make_foo(): # @make_foo()
push rbx
sub rsp, 16
mov rbx, rdi
lea rdi, [rsp + 8]
call foo_factory()
cmp byte ptr [rsp + 9], 0
je .LBB0_2
mov byte ptr [rsp + 9], 0
call on_destroy()
.LBB0_2:
mov rax, rbx
add rsp, 16
pop rbx
ret
【问题讨论】:
-
你无法避免破坏。
std::optional< foo >中有一个foo。即使foo被移动,可选的仍然必须销毁剩下的存根。 -
移出的实例仍然是一个实例。当
optional被销毁时,它将被销毁,即使该销毁没有什么可清理的。你的析构函数应该检查一个被移动的实例。如果你的类型支持移动语义,那么析构函数总是做一些有意义的事情是非常可疑的。 -
检查是因为编译器不知道前置条件,它需要知道“选择”正确的析构函数。
-
为了让这个实验更有趣,写一个更真实的测试类,带有移动语义和一个可选的
on_destroy调用,只有当对象没有被移动时才会发生。现在优化器的挑战是检测make_foo中的移动,将该状态跟踪到dtor,并消除对on_destroy的调用。 -
您无法摆脱析构函数调用,因为移出的对象仍然是对象,并且
on_destroy调用在此上下文中对编译器是不透明的并且不能内联 - 但是您可以通过使用__builtin_unreachable向编译器提示该分支始终是特定情况来摆脱分支。 (godbolt link)
标签: c++ c++17 destructor move-semantics stdoptional