【问题标题】:How to move a value out of a std:optional without calling the destructor?如何在不调用析构函数的情况下将值移出 std:optional ?
【发布时间】:2021-06-19 04:30:51
【问题描述】:

我正在尝试编写一个函数make_foo,它将“解开”std::optional< foo >,返回包含的值。 该函数假定启用了可选项,因此不对optional 执行任何运行时检查。

下面是我的实现,以及已编译的程序集以供参考。 我有几个关于编译器输出的问题:

  1. 为什么这会导致代码分支? optional::operator* 提供对包含值的未经检查的访问,所以我不希望看到任何分支。

  2. 为什么会调用foo 的析构函数?请注意程序集中对on_destroy() 的调用。我们如何在不调用析构函数的情况下将包含的值移出可选项?

Godbolt link

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&lt; foo &gt; 中有一个 foo。即使 foo 被移动,可选的仍然必须销毁剩下的存根。
  • 移出的实例仍然是一个实例。当optional 被销毁时,它将被销毁,即使该销毁没有什么可清理的。你的析构函数应该检查一个被移动的实例。如果你的类型支持移动语义,那么析构函数总是做一些有意义的事情是非常可疑的。
  • 检查是因为编译器不知道前置条件,它需要知道“选择”正确的析构函数。
  • 为了让这个实验更有趣,写一个更真实的测试类,带有移动语义和一个可选的on_destroy调用,只有当对象没有被移动时才会发生。现在优化器的挑战是检测make_foo 中的移动,将该状态跟踪到dtor,并消除对on_destroy 的调用。
  • 您无法摆脱析构函数调用,因为移出的对象仍然是对象,并且 on_destroy 调用在此上下文中对编译器是不透明的并且不能内联 - 但是您可以通过使用__builtin_unreachable 向编译器提示该分支始终是特定情况来摆脱分支。 (godbolt link)

标签: c++ c++17 destructor move-semantics stdoptional


【解决方案1】:

如何在不调用析构函数的情况下将值从 std:optional 中移出?

就像您在示例中所做的那样。移动不调用析构函数。 foo 的析构函数由 std::optional 的析构函数调用,该析构函数由您创建的临时 std::optional 对象的析构调用。

您只能通过泄漏对象来防止对象被破坏,或者首先避免创建(以及移动)对象。

为什么这会导致代码分支?

std::optional 的析构函数中有一个分支。仅当std::optional 不为空时才调用所包含对象的析构函数。

optional::operator* 提供对包含值的未经检查的访问,所以我不希望看到任何分支。

理论上,如果优化器足够聪明,它可能会使用这些知识无条件地调用析构函数,因为如果函数返回空的std::optional,它可能知道程序的行为是未定义的。它似乎还不够聪明,无法进行这样的优化。

【讨论】:

    【解决方案2】:

    你的方法或多或少是这样的:

    foo make_foo() {
       auto x = foo_factory(); 
       return *x;
    }
    

    其中x 是从工厂返回的optional(在您的代码中暂时未命名)。当x 被销毁时,它要么调用所包含对象的析构函数(当有一个时)。或者它不会破坏包含的对象(当没有对象时)。简而言之:您从中移出的 foo 仍然需要被销毁,即使您知道可选项确实包含它,编译器也不能,因此分支。

    如何在不调用析构函数的情况下将值从 std:optional 中移出?

    你不能。即使是移动的对象最终也需要销毁。

    【讨论】:

      猜你喜欢
      • 2012-07-03
      • 2011-03-11
      • 2012-10-04
      • 2014-08-03
      • 1970-01-01
      • 1970-01-01
      • 2023-04-04
      • 2020-11-22
      相关资源
      最近更新 更多