Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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 "contains" 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.

like image 698
Drew Dormann Avatar asked Sep 16 '19 20:09

Drew Dormann


People also ask

What is std :: Nullopt in C++?

(since C++17) std::nullopt is a constant of type std::nullopt_t that is used to indicate optional type with uninitialized state.

What is std :: optional for?

The class template std::optional manages an optional contained value, i.e. a value that may or may not be present. A common use case for optional is the return value of a function that may fail.

Does std :: optional use heap?

First of all, it doesn't require allocation on the heap -- it can be stored along side other data.

Does STD optional allocate memory?

What's more, std::optional doesn't need to allocate any memory on the free store. std::optional is a part of C++ vocabulary types along with std::any , std::variant and std::string_view .


3 Answers

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.

like image 136
T.C. Avatar answered Oct 11 '22 01:10

T.C.


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.

like image 23
Marc Glisse Avatar answered Oct 11 '22 02:10

Marc Glisse


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)))

like image 3
ph3rin Avatar answered Oct 11 '22 00:10

ph3rin