0

The problem

I have made some tests on compiler-explorer about std::optional, and to my surprise, it seems like it behaves like a pointer, even though it is stated in the standard (§23.6.3) that it should contain it:

Implementations are not permitted to use additional storage, such as dynamic memory, to allocate its contained value.

The tests

The full code I tested is on this compiler-explorer sheet, although I don't know how much time it will remain up. That is why I will describe here the tests I made.

What I am testing

I am making two tests with one function each:

  • Checking if the value is there
  • Checking if the value is there and retrieval of it if it is there, or 67780 otherwise

I used -O2 minimum and --std=c++17 as compiler flags for both gcc 7.2 and clang 5.0.0 on for the x86_64 target. The results copied here are from clang.

Using std::optional

Checking

Code:

bool check(const std::optional<int> maybe_int) {
    return maybe_int.has_value();
}

Result:

mov al, byte ptr [rdi + 4]
ret

One indirection.

Retrieving

Code:

int retrieve(const std::optional<int> maybe_int) {
    if(maybe_int.has_value())
        return maybe_int.value();
    else
        return 67780;
}

Result:

cmp byte ptr [rdi + 4], 0
je .LBB1_1
mov eax, dword ptr [rdi]
ret
.LBB1_1:
mov eax, 67780
ret

One indirection for checking, one for retrieving.

Using a custom class

The class

template<typename T>
class my_optional {
private:
    T val;
    bool has_val;
public:
    /* Constuctors ... */

    bool has_value() const {
        return has_val;
    }
    decltype(auto) value() const {
        return val;
    }
};

Checking

Code:

bool check(const my_optional<int> maybe_int) {
    return maybe_int.has_value();
}

Result:

shr rdi, 32
test dil, dil
setne al
ret

No indirections.

Retrieving

Code:

int retrieve(const my_optional<int> maybe_int) {
    if(maybe_int.has_value())
        return maybe_int.value();
    else
        return 67780;
}

Result:

movabs rax, 1095216660480
test rdi, rax
mov eax, 67780
cmovne eax, edi
ret

Although I don't know how that works, it does not have any indirections.

The questions

Either the title or "What is wrong with my tests ?"

Jester
  • 56,577
  • 4
  • 81
  • 125
ll-h
  • 11
  • 2

1 Answers1

-2

Implementations are not permitted to use additional storage, such as dynamic memory, to allocate its contained value.

I read that as: they are not allowed to use additional storage on top of the storage for the base of std::optional. But that std::optional<int> looks to be implemented as struct { int value; byte has_value; }; and passed as reference (pointer) to the code (from the machine code point of view, it's "pass by value" in C++ terms, but the actual implementation for CPU is using one level of indirection any way), thus any access to the content needs indirection from that reference/pointer.

But if you will set it from empty to value, or delete value, it will not change allocated storage, it will keep those 5 bytes of storage in any case (probably padded to 8).

Your custom class does use 8 bytes of storage, working with it as whole qword, extracting the has_value and value by arithmetic (bit shifting and masking), looks to be somewhat better, can't think of particular disadvantage against the std variant in 15 seconds.

And it's passed as value, not reference.


That standard wording would be broken, if the storage of std::optional<int> would be only 4 bytes, and it would work itself as pointer to dynamically allocated memory, i.e. empty it would contain nullptr, and upon storing int value into it, it would allocated dynamic memory somewhere, and the original 4B storage would keep the pointer to the new memory.

Ped7g
  • 16,236
  • 3
  • 26
  • 63
  • Didn't I pass the std::optional by value ? – ll-h Nov 21 '17 at 08:18
  • @ll-h ah, sorry for being misleading in the answer, I was describing it from the assembly point of view, not C++. From C++ point of view you pass it by value, the duplicate answer gives better answer why the C++ language does treat `std::optional` as more complex type, and will assemble it as pointer to instance instead of trivially passing the 64bit value around. It's probably like when you pass `struct` by value, it's still technically pointer into memory (copy on stack for trivial structs). I'm not C++ expert, I was more explaining what the assembly does, sorry for confusion. – Ped7g Nov 21 '17 at 10:57
  • @ll-h: Like the duplicate explains, (and @ Barry's answer before he deleted it), libstdc++'s `std::optional` is not trivially-copyable even if `T` is, and thus the x86-64 C++ ABI used by gcc/clang pass it by hidden pointer. It's not the complexity of the data members in the class, just the way the constructor / destructor are written. (libc++'s implementation does make it trivially-copyable when `T` is.) – Peter Cordes Nov 21 '17 at 12:08
  • @ll-h btw, I tried mostly to explain what does standard wording means (how it is not broken by that indirection code), but in case you got nothing from my answer, let me know, what's confusing/etc. :) – Ped7g Nov 21 '17 at 12:51