I've been trying to understand the rationale for std::nullopt_t to not be allowed to be DefaultConstructible in C++17 (where it was introduced) and beyond, and stepped over some compiler variance confusion in the process.
Consider the following spec-violating (it is DefaultConstructible) implementation of nullopt_t:
struct nullopt_t {
    explicit constexpr nullopt_t() = default;
};
which is an aggregate in C++11 and C++14 (no user-provided ctors), but which is not an aggregate in C++17 (explicit ctor) and C++20 (user-declared ctor).
Now consider the following example:
struct S {
    constexpr S() {}
    S(S const&) {}
    S& operator=(S const&) { return *this; }   // #1
    S& operator=(nullopt_t) { return *this; }  // #2
};
int main() {
    S s{};
    s = {};  // GCC error: ambiguous overload for 'operator=' (#1 and #2)
}
This is rejected by GCC (various versions, say v11.0) throughout C++11 to C++20, but is accepted by both Clang (say v12.0) and MSVC (v19.28) throughout C++11 to C++20.
DEMO
My initial assumptions were that the program:
nullopt_t (as above) is an aggregate, whereas itnullopt_t object as needed for the copy assignment operator at #2 to be viable,but none of the compilers agree in full with this theory, some I'm probably missing something.
What compiler is correct here (if any), and how do we explain it by relevant standard sections (and DR:s, if relevant)?
nullopt_t required to be DefaultConstructible in the first place?The spec requirement that nullopt_t shall not be DefaultConstructible is arguably, in retrospect, a mistake based on some LWG and CWG confusion around tag types, and the resolution of this confusion which came only after std::optional was brought in from the Library Fundamentals TS Components.
First of all, the current (C++17, C++20) spec of nullopt_t, [optional.nullopt]/2, requires [emphasis mine]:
Type
nullopt_tshall not have a default constructor or an initializer-list constructor, and shall not be an aggregate.
and its main use is described in the previous section, [optional.nullopt]/1:
[...] In particular,
optional<T>has a constructor withnullopt_tas a single argument; this indicates that an optional object not containing a value shall be constructed.
Now, P0032R3 (Homogeneous interface for variant, any and optional), one of the papers which was part of introducing std::optional, has a discussion around nullopt_t, tag types in general, and the DefaultConstructible requirement [emphasis mine]:
No default constructible
While adapting
optional<T>to the newin_place_ttype we found that we cannot anymore usein_place_t{}. The authors don't consider this a big limitation as the user can usein_placeinstead. It needs to be noted that this is in line with the behavior ofnullopt_tasnullopt_t{}fails as no default constructible. Howevernullptr_t{}seems to be well formed.Not assignable from
{}After a deeper analysis we found also that the old
in_place_tsupportedin_place_t t = {};. The authors don't consider this a big limitation as we don't expect that a lot of users could use this and the user can usein_placeinstead.in_place_t t; t = in_place;It needs to be noted that this is in line with the behavior of
nullopt_tas the following compile fails.nullopt_t t = {}; // compile failsHowever
nullptr_tseems to be support it.nullptr_t t = {}; // compile passTo re-enforce this design, there is an pending issue 2510-Tag types should not be
DefaultConstructibleCore issue 2510.
And indeed, the initial proposed resolution of LWG Core Issue 2510 was to require all tag types to not be DefaultConstructible [emphasis mine]:
(LWG) 2510. Tag types should not be
DefaultConstructible[...]
Previous resolution [SUPERSEDED]:
[...] Add a new paragraph after 20.2 [utility]/2 (following the header synopsis):
- -?- Type
 piecewise_construct_tshall not have a default constructor. It shall be a literal type. Constantpiecewise_constructshall be initialized with an argument of literal type.
This resolution was superseded, however, as there were overlap with CWG Core Issue 1518, which was eventually resolved in a way that did not require tag types to not be DefaultConstructible, as explicit would suffice [emphasis mine]:
(CWG) 1518. Explicit default constructors and copy-list-initialization
[...]
Additional note, October, 2015:
It has been suggested that the resolution of issue 1630 went too far in allowing use of explicit constructors for default initialization, and that default initialization should be considered to model copy initialization instead. The resolution of this issue would provide an opportunity to adjust that.
Proposed resolution (October, 2015):
Change 12.2.2.4 [over.match.ctor] paragraph 1 as follows:
[...] For direct-initialization
or default-initialization, the candidate functions are all the constructors of the class of the object being initialized. [...]
as long as explicit also implied that the type was not an aggregate, which in turn was the final resolution of LWG Core Issue 2510 (based on the final resolution of CWG Core Issue 1518)
(LWG) 2510. Tag types should not be
DefaultConstructible[...]
Proposed resolution:
[...] In 20.2 [utility]/2, change the header synopsis:
// 20.3.5, pair piecewise construction struct piecewise_construct_t { explicit piecewise_construct_t() = default; }; constexpr piecewise_construct_t piecewise_construct{};[...]
These latter changes, however, were not brought into the proposal for std::optional, arguably an oversight, and I would like to claim that nullopt_t need not be required to not be DefaultConstructible, only, like other tag types, that it should have a user-declared explicit constructor, which bans it from a candidate for empty-braces copy-list-init both by it not being an aggregate and by the only candidate constructor being explicit.
Given the LWG 2510, CWG 1518 (and other) confusion, let's focus on C++17 and beyond. In this case, GCC is arguably wrong to reject the program, whereas Clang and MSVC are correct to accept it.
Why?
Because the S& operator=(nullopt_t) assignment operator is not viable for the assignment s = {};, as the empty braces {} would require either aggregate initialization or copy-list-initialization to create a nullopt_t (temporary) object. nullopt_t, however (by the idiomatic tag implementation: my implementation above), as per as per P0398R0 (which resolves CWG Core Issue 1518), is neither an aggregate nor does its default constructor participate in copy-list-initialization (from empty braces).
This likely falls under the following GCC bug report:
which was listed as SUSPENDED in 2015-06-15, before the change in the resolution of CWG Core Issue 1630 ("resolution of issue 1630 went too far"). The ticket is now re-opened based on a ping from this Q&A.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With