问题
I thought that initializing a std::optional with std::nullopt would be the same as default construction.
They are described as the same 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 "contains" flag,
nullopt_init():
mov BYTE PTR [rdi+65536], 0
mov rax, rdi
ret
While default construction will value initialize the entire 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?
回答1:
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.
Now 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 make the default constructor trivial (libstdc++), which has some advantages but forces zero-initialization of the whole buffer; or they provide a default constructor (libc++) that initializes only what is needed (like the constructor from std::nullopt
), but they lose triviality. Sadly, it does not seem possible to have the advantages of both. I think I prefer the second version. In the mean time, pragmatically, using the constructor from std::nullopt
where it does not complicate code seems to be a good idea.
回答2:
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.
回答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;
- Ensures:
*this
does not contain a value. - Remarks: No contained value is initialized. For every object type
T
these constructors shall beconstexpr
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))
)
回答4:
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.
来源:https://stackoverflow.com/questions/57964217/stdoptional-construct-empty-with-or-stdnullopt