Why is 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_t shall 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 with nullopt_t as 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 new in_place_t type we found that we cannot anymore use in_place_t{}. The authors don't consider this a big limitation as the user can use in_place instead. It needs to be noted that this is in line with the behavior of nullopt_t as nullopt_t{} fails as no default constructible. However nullptr_t{} seems to be well formed.
Not assignable from {}
After a deeper analysis we found also that the old in_place_t supported in_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 use in_place instead.
in_place_t t; t = in_place;
It needs to be noted that this is in line with the behavior of nullopt_t as the following compile fails.
nullopt_t t = {}; // compile fails
However nullptr_t seems to be support it.
nullptr_t t = {}; // compile pass
To re-enforce this design, there is an pending issue 2510-Tag types should not be DefaultConstructible Core 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_t shall not have a default constructor. It shall be a literal type. Constant piecewise_construct shall 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:
[...]
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.
Which compiler is right and wrong here?
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.