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:
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.Why does
foo
's destructor get called? Note the call toon_destroy()
in the assembly. How do we move the contained value out of the optional without calling the destructor?
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
std::optional< foo >
has afoo
in it. Even if thatfoo
gets moved, the optional still has to destroy the stub that is left. – Sargeantoptional
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. – Cuspidorvector
). 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. – Sargeantbool
member variable tofoo
which indicates whether the cleanup in the destructor is required or not. – Gildeaon_destroy
that only happens if the object was not moved from. Now the optimizer challenge is to detect a move inmake_foo
, track that state to the dtor, and eliminate the call toon_destroy
there. – HammondNULL
for pointer-type handles or something like Windows API'sINVALID_HANDLE
for integer-type handles. Check the C API you are using for such constants. – Cuspidorstd::optional
with astd::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 usestd::optional
at all? – Walcotton_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