std::optional - construct empty with {} or std::nullopt?
Asked Answered
H

4

73

I thought that initializing a std::optional with std::nullopt would be the same as default construction.

They are described as equivalent at cppreference, as form (1)

However, both Clang and GCC seem to treat these toy example functions differently.

#include <optional>

struct Data { char large_data[0x10000]; };

std::optional<Data> nullopt_init()
{
  return std::nullopt;
}

std::optional<Data> default_init()
{
  return {};
}

Compiler Explorer seems to imply that using std::nullopt will simply set the one byte "has_value" flag,

nullopt_init():
    mov     BYTE PTR [rdi+65536], 0
    mov     rax, rdi
    ret

While default construction will value-initialize every byte of the class. This is functionally equivalent, but almost always costlier.

default_init():
    sub     rsp, 8
    mov     edx, 65537
    xor     esi, esi
    call    memset
    add     rsp, 8
    ret

Is this intentional behavior? When should one form be preferred over the other?


Update: GCC (since v11.1) and Clang (since v12.0.1) now treat both forms efficiently.

Hodeida answered 16/9, 2019 at 20:48 Comment(6)
You should use std::nullopt most of the time because it's more explicit and reduces confusion and as you shown, sometimes {} acts as default init instead of what you think it would.Vegetable
As a comment on my own question, to save anyone the trouble of researching this... the slower form is generated regardless of using Clang or GCC; regardless of initializing with {} or (); regardless of whether Data is Plain Old Data or has a defined default constructor.Hodeida
Here is an isolated toy example (based on libstdc++ implementation) showing this behavior. I added some alternative constructors. I don't know why nullopt1 and nullopt2 act differently, but that's probably not in the scope of this question.Aimo
On second thought, this is more of a half-dupe than a proper dupe of this question. But - I can't undo the dupe-marking for some reason.Runabout
@Runabout thanks for leaving a note. The question here arose from all the answers you linked to. I was able to un-dupe your flag here.Hodeida
@DrewDormann your clang-12 is using libstdc++ from gcc-10. If you make it use a more recent libstdc++ (or libc++) I expect it will work better. On godbolt, clang-trunk appears to be configured to use a recent libstdc++, you can also try that.Eon
D
30

In this case, {} invokes value-initialization. If optional's default constructor is not user-provided (where "not user-provided" means roughly "is implicitly declared or explicitly defaulted within the class definition"), that incurs zero-initialization of the entire object.

Whether it does so depends on the implementation details of that particular std::optional implementation. It looks like libstdc++'s optional's default constructor is not user-provided, but libc++'s is.

Dragnet answered 16/9, 2019 at 20:55 Comment(5)
"is not user-provided" Aka =defaulted in class body (in this case).Triliteral
I believe optional's default constructor is required to be provided. Is cppreference wrong here describing the constructor constexpr optional() noexcept;?Hodeida
That doesn't preclude an implementation from =default;ing it. I don't think it's observable in a conforming program, so as-if applies.Dragnet
I can infer the answer to "When should one form be preferred over the other?", but would you perhaps like to provide one? There seem to be several posts here that imply equivalence, but that seems not to be the case.Hodeida
"that incurs zero-initialization of the entire object." -- would that apply even if Data had a user-provided default constructor?Trici
E
21

For gcc, the unnecessary zeroing with default initialization

std::optional<Data> default_init() {
  std::optional<Data> o;
  return o;
}

is bug 86173 and needs to be fixed in the compiler itself. Using the same libstdc++, clang does not perform any memset here.

In your code, you are actually value-initializing the object (through list-initialization). It appears that library implementations of std::optional have 2 main options: either they default the default constructor (write =default;, one base class takes care of initializing the flag saying that there is no value), like libstdc++, or they define the default constructor, like libc++.

Now in most cases, defaulting the constructor is the right thing to do, it is trivial or constexpr or noexcept when possible, avoids initializing unnecessary things in default initialization, etc. This happens to be an odd case, where the user-defined constructor has an advantage, thanks to a quirk in the language in [decl.init], and none of the usual advantages of defaulting apply (we can specify explicitly constexpr and noexcept). Value-initialization of an object of class type starts by zero-initializing the whole object, before running the constructor if it is non-trivial, unless the default constructor is user-provided (or some other technical cases). This seems like an unfortunate specification, but fixing it (to look at subobjects to decide what to zero-initialize?) at this point in time may be risky.

Starting from gcc-11, libstdc++ switched to the used-defined constructor version, which generates the same code as std::nullopt. In the mean time, pragmatically, using the constructor from std::nullopt where it does not complicate code seems to be a good idea.

Eon answered 27/9, 2019 at 17:34 Comment(1)
optional's default constructor can't be trivial; you need to initialize the "has value" flag.Dragnet
D
3

The standard doesn't say anything about the implementation of those two constructors. According to [optional.ctor]:

constexpr optional() noexcept;
constexpr optional(nullopt_t) noexcept;
  1. Ensures:*this does not contain a value.
  2. Remarks: No contained value is initialized. For every object type T these constructors shall be constexpr constructors (9.1.5).

It just specifies the signature of those two constructors and their "Ensures" (aka effects): after any of those constructions the optional doesn't contain any value. No other guarantees are given.

Whether the first constructor is user-defined is implementation-defined (i.e depends on the compiler).

If the first constructor is user-defined, it can of course be implemented as setting the contains flag. But a non-user-defined constructor is also compliant with the standard (as implemented by gcc), because this also zero-initialize the flag to false. Although it does result in costy zero-initialization, it doesn't violate the "Ensures" specified by the standard.

As it comes to real-life usage, well, it is nice that you have dug into the implementations so as to write optimal code.

Just as a side-note, probably the standard should specify the complexity of those two constructors (i.e O(1) or O(sizeof(T)))

Dhiman answered 28/9, 2019 at 21:35 Comment(0)
D
0

Motivational example

When I write:

std::optional<X*> opt{};
(*opt)->f();//expects error here, not UB or heap corruption

I would expect the optional is initialized and doesn't contain uninitialized memory. Also I wouldn't expect a heap corruption to be a consequence since Im expecting everything is initialized fine. This compares up with the pointer semantic of std::optional:

X* ptr{};//ptr is now zero
ptr->f();//deterministic error here, not UB or heap corruption

If I write std::optional<X*>(std::nullopt) I would have hoped the same but at least here it looks more of an ambiguous situation.

The reason is Uninitialized Memory

It is very likely that this behavior is intentional.

(Im not part of any comittee so in the end I cannot say sure)

This is the primary reason: an empty brace init (zero-init) shouldn't lead to uninitialized memory (although the language doesn't enforce this as a rule) - how else will you guarentee there's no un-initialized memory in your program ?

For this task we often turn to use static analysis tools: prominently cpp core check that is based on enforcing the cpp core guidelines; in particular there's a few guidelines concerning exactly this issue. Had this not been possible our static analysis would fail for this otherwise seemingly simple case; or worse be misleading. In contrast, heap based containers do not have the same issue naturally.

Unchecked access

Remember that accessing std::optional is unchecked - this leads to the case where you could by mistake access that unitialized memory. Just to showcase this, if that weren't the case then this could be heap corruption:

std::optional<X*> opt{};//lets assume brace-init doesn't zero-initialize the underlying object for a moment (in practice it does)
(*opt)->f();//<- possible heap corruption

With current implementation however, this becomes deterministic (seg fault/access violation on main platforms).


Then you might ask, why doesn't the std::nullopt 'specialized' constructor not initialize the memory ?

Im not really sure why it doesn't. While I guess it wouldn't be an issue if it did. In this case, as opposed to the brace-init one, it doesn't come with the same kind of expectations. Subtly, you now have a choice.

For those interested MSVC does the same.

Dreamworld answered 27/9, 2019 at 9:54 Comment(3)
"a guarenteed seg fault" - no it isn't. I've worked on platforms where writing through a null pointer doesn't cause a seg-fault; it just sets an interrupt vector.Amadeus
Although this may work fine in practice, your first example invokes UB as *this doesn't contain a value. Also, you could have the same property with std::optional<X*>{{}}, where you explicitly zero-initialize when it makes sense and you really need it.Aimo
Slowing down the program so bugs are a bit more likely to be found (are they? tooling is getting quite good at detecting uses of uninitialized memory) would be fine for some debug mode, not for the maximum performance mode.Eon

© 2022 - 2024 — McMap. All rights reserved.