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_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 withnullopt_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 newin_place_t
type we found that we cannot anymore usein_place_t{}
. The authors don't consider this a big limitation as the user can usein_place
instead. It needs to be noted that this is in line with the behavior ofnullopt_t
asnullopt_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_t
supportedin_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_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. Constantpiecewise_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:
// 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