How to move a value out of a std:optional without calling the destructor?
Asked Answered
G

2

6

I am trying to write a function, make_foo, that will "unwrap" a std::optional< foo >, returning the contained value. The function assumes that the optional is engaged so does not perform any runtime checks on the optional.

My implementation of this is below, along with the compiled assembly for reference. I have a couple of questions about the compiler output:

  1. Why does this result in branching code? optional::operator* gives unchecked access to the contained value, so I would not expect to see any branching.

  2. Why does foo's destructor get called? Note the call to on_destroy() in the assembly. How do we move the contained value out of the optional without calling the destructor?

Godbolt link

C++17 source

#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();
}

Optimized compiler output (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
Gildea answered 22/3, 2021 at 14:2 Comment(9)
You can't avoid the destruction. std::optional< foo > has a foo in it. Even if that foo gets moved, the optional still has to destroy the stub that is left.Sargeant
A moved-from instance is still an instance. It will get destroyed when the optional is destroyed, even if that destruction has nothing to clean up. Your destructor should check for a moved-from instance. If your type supports move semantics, it would be very suspicious for that destructor to always do something meaningful.Cuspidor
and the check is because the pre-condition is unknown from compiler, and it need to know that to "select" correct destructor.Abelabelard
This might help you understand moving a little bit better. Lets pretend we a cardboard box that has things inside it (this models a vector). When you move that box, you don't actually move the box. What you do is open a new empty box, scoop out the contents of the old box, and put them into the new box. When you are done, the old box is still there, it's just empty. This is what moving does. After the move is complete, you need to get rid of that empty box, and that happens via a call to the destructor of the box.Sargeant
Thanks for the comments. That has helped clarify what is going on. @FrançoisAndrieux, in my actaul code the class is an RAII-wrapper around two C API functions (initialize library in constructor, clean up library in destructor). From what you have suggested, I think the solution for me will be to add a bool member variable to foo which indicates whether the cleanup in the destructor is required or not.Gildea
To make this experiment more interesting, write a more realistic test class with move semantics and an optional call to on_destroy that only happens if the object was not moved from. Now the optimizer challenge is to detect a move in make_foo, track that state to the dtor, and eliminate the call to on_destroy there.Hammond
@Gildea That should work. But typically, C API handle types have specific values to indicate "no object" such as NULL for pointer-type handles or something like Windows API's INVALID_HANDLE for integer-type handles. Check the C API you are using for such constants.Cuspidor
If you swap std::optional with a std::unique_ptr the instance is not destroyed. However, this will add dynamic allocation... Anyhow, if "Pre-condition: Call to foo_factory() will not return nullopt", why use std::optional at all?Walcott
You can't get rid of the destructor call because moved-from objects are still objects, and the on_destroy call is opaque to the compiler in this context and can't be inlined -- but you can get rid of the branch by hinting to the compiler that the branch is always a specific case using __builtin_unreachable. (godbolt link)Resile
D
3

How to move a value out of a std:optional without calling the destructor?

Like you did in the example. The destructor is not called by the move. The destructor of foo is called by the destructor of the std::optional which is called by the destruction of the temporary std::optional object that you created.

You can only prevent an object from being destroyed by leaking it, or by avoiding creation (and thus also the move) of the object in the first place.

Why does this result in branching code?

There is a branch in the destructor of std::optional. The destructor of the contained object is called only if the std::optional is not empty.

optional::operator* gives unchecked access to the contained value, so I would not expect to see any branching.

In theory, if the optimiser was smart enough, it might use this knowledge to call the destructor unconditionally, since it might know that behaviour of the program is undefined if the function returned an empty std::optional. It seems to not have been smart enough to make such optimisation.

Drudgery answered 22/3, 2021 at 14:22 Comment(0)
D
3

Your method is more or less this:

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

Where x is the optional returned from the factory (unnamed temporary in your code). When x is destroyed, it either calls the destructor of the contained object (when there is one). Or it does not destroy the contained object (when there is none). In short: The foo you moved from still needs to be destroyed and even though you know that the optional does contain it, the compiler cannot, hence the branch.

How to move a value out of a std:optional without calling the destructor?

You can't. Even a moved from object needs to be destroyed eventually.

Decal answered 22/3, 2021 at 14:6 Comment(0)
D
3

How to move a value out of a std:optional without calling the destructor?

Like you did in the example. The destructor is not called by the move. The destructor of foo is called by the destructor of the std::optional which is called by the destruction of the temporary std::optional object that you created.

You can only prevent an object from being destroyed by leaking it, or by avoiding creation (and thus also the move) of the object in the first place.

Why does this result in branching code?

There is a branch in the destructor of std::optional. The destructor of the contained object is called only if the std::optional is not empty.

optional::operator* gives unchecked access to the contained value, so I would not expect to see any branching.

In theory, if the optimiser was smart enough, it might use this knowledge to call the destructor unconditionally, since it might know that behaviour of the program is undefined if the function returned an empty std::optional. It seems to not have been smart enough to make such optimisation.

Drudgery answered 22/3, 2021 at 14:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.