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:
is ill-formed in C++11 and C++14, as nullopt_t (as above) is an aggregate, whereas it
is well-formed in C++17 and C++20, as it is no longer an aggregate, meaning that its explicit default constructor should prohibit copy-list-init of a temporary nullopt_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)?
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:
// 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.
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:
Bug 54835 - (C++11)(DR 1518) Explicit default constructors not respected during copy-list-initialization
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.
Related
What std::is_constructible shall return for an aggregate type that does not allow creation of objects due to invalid initializer of a member field?
Consider for example
#include <type_traits>
template <class T>
struct A {
T x{};
};
static_assert( !std::is_constructible_v<A<int&>> );
A<int&> obj; is not well-formed since it cannot initialize int& from {}. So I would expect that the example program above compiles fine, as it does in GCC. But MSVC accepts the opposite statement static_assert( std::is_constructible_v<A<int&>> ); presumable since the default constructor of A was not formally deleted. And Clang behaves in the third way stopping the compilation with the error:
error: non-const lvalue reference to type 'int' cannot bind to an initializer list temporary
T x{};
^~
: note: in instantiation of default member initializer 'A<int &>::x' requested here
struct A {
^
Online demo: https://gcc.godbolt.org/z/nnxcGn7WG
Which one of the behaviors is correct according to the standard?
std::is_constructible_v<A<int&>> checks whether a variable initialization with () initializer is possible. That's never aggregate initialization (not even in C++20), but always value initialization, which will use the default constructor.
Because your class doesn't declare any constructor, it still has an implicit default constructor declared. That one will be called during value initialization.
The implicit default constructor is also not defined as deleted, because none of the points in [class.default.ctor]/2 apply. There is one point for reference members without any default member initializer though.
So this constructor will be chosen in overload resolution and therefore std::is_constructible_v<A<int&>> is true. That the instantiation of the default constructor might be ill-formed is irrelevant. Only declarations are checked (the immediate context). So GCC's behavior is not correct.
The only remaining question now is whether the implicit default constructor (or rather the default member initializer?) is supposed to be instantiated from the std::is_constructible test itself, causing the program to be ill-formed.
As far as I can tell the standard doesn't specify that clearly. The standard says that the noexcept-specifier of a function is implicitly instantiated when it is needed. (see [temp.inst]/15)
It also says that it is needed if it is used in an unevaluated operand in a way that would be an ODR use if it was potentially-evaluated. (see [except.spec]/13.2
Arguably the type trait has to make such a use.
Then [except.spec]/7.3 specifies that it needs to be checked whether the default member initializers are potentially-throwing in order to check whether the implicit default constructor has a potentially-throwing exception specification.
Clang seems to then follow the idea that this requires instantiation of the default member initializer and therefore causes the compilation error because that instantiation is invalid.
The problems I see with that is:
I don't see anything in the [temp.inst] about when default member initializers are instantiated or what that would mean exactly.
[temp.inst]/15 speaks of the noexcept-specifier grammar construct and since the default constructor is implicit, that doesn't really work out.
The standard states:
Only the validity of the immediate context of the variable initialization is considered.
and
The evaluation of the initialization can result in side effects such as the instantiation of class template specializations and function template specializations, the generation of implicitly-defined functions, and so on.
Such side effects are not in the “immediate context” and can result in the program being ill-formed.
It is the generation of an the definition for an implicity-defined function (the constructor) that fails in your example. So this is not the "immediate context". That would allow:
The msvc interpretation: It doesn't consider the validity of the non-immediate context
The clang interpretation: The side effects result in your program being ill-formed
If anything it seems to me that gcc's result might be incorrect. But I'm not sure if the standard wants to forbid the consideration of the "non-immediate context" or if it simply doesn't require it.
cppreference says for zero initialization:
Zero initialization is performed in the following situations:
...
2) As part of value-initialization sequence for non-class types and for members of value-initialized class types that have no constructors, ...
What trait or combination of traits can I use to test either for:
type is zero-initializable with an empty {}, or
class type has "no constructors"
(which I think means no user-defined constructors?)
(P.S. tagged C++11 to indicate any "modern C++" approach is acceptable; also tagged C++17 to indicate the latest C++ version I'd like for an answer.)
(P.P.S. I know cppreference is not the standard, but it is almost always correct. If it is not correct in this case, that should be part of the answer ...)
Let's say, for reasons of my own, I want a class to have a non-static reference member. I would think that this type should be easily optimized out of most code that uses it. Therefore, I assert its triviality in a unit test.
Clang and GCC agree that the class is trivial, but MSVC disagrees. Who is right, and why, according to the standard?
#include <type_traits>
struct example {
int& r;
};
// Clang and GCC let this pass
// MSVC fires this assertion
static_assert(
std::is_trivial<example>::value,
"example is not trivial"
);
According to C++17 [class]/6, for a class to be trivial, among other requirements, it has to have at least one non-deleted default constructor. The example class's default constructor is deleted, so example is not a trivial class.
Before C++17, the situation is somewhat less clear. It was required for a trivial class to have a trivial default constructor, and it was not clear whether a deleted default constructor qualifies as trivial. Richard Smith asked in CWG 1928 whether special member functions that are defaulted and implicitly deleted are trivial. The Committee's response was:
CWG feels that the triviality of a deleted function should be irrelevant. Any cases in which the triviality of a deleted function is observable should be amended to remove that dependency.
Subsequently, the resolution of CWG 1496 resolved this issue in the case of trivial classes: it no longer matters whether a deleted default constructor qualifies as trivial, because in C++17, the definition says that all default constructors (in case there are multiple) must be either trivial or deleted and at least one must be non-deleted.
It seems that GCC and Clang may have been considering deleted default constructors as trivial, and haven't been updated with the C++17 definition of "trivial class". You should file bug reports.
This question is regarding: template<class...Types> class variant:
According to variant.variant/3, a program instantiating variant with no template arguments is ill-formed.
So far, so clear. Now I have a question regarding the converting constructor (template<class T> constexpr variant(T&& t) noexcept(see below)):
variant.variant/variant.ctor-16.1 says that the converting constructor shall not participate in overload resolution unless:
sizeof...(Types) is nonzero
(... and some other requirements I do not care about for now).
My question is, when a variant with no template arguments already makes my program ill-formed, why still care about whether my converting constructor participates in overload resolution or not?
Having a look at the MSVC and libstdc++ -implementation of variant they actually have an enable_if_t<sizeof...(_Types) != 0> at the declaration of the converting constructor. Why?
The clause "sizeof...(Types) is nonzero" was added to [variant.ctor] as part of the paper: Some improvements to class template argument deduction integration into the standard library to allow variant support as well.
Relevant Excerpts:
Enable variant support
The following code fails to compile
variant<int, double> v1(3);
variant v2 = v1; // Ill-formed! <--THIS
As this natural code is useful and its failure is confusing, we propose that it be supported. Indeed, prior to the adoption of p0510r0 banning variant<>, the above code worked as expected since variant<> occurs in some deduction guides in the overload set. As it is not clear that constructor template argument deduction was considered in adopting p0510r0, we would like to consider allowing variant<> not to produce a hard error in such cases.
Wording (Emphasis added)
Change §23.7.3.1p16 [variant.ctor] as follows:
Remarks: This function shall not participate in overload resolution unless sizeof...(Types) is nonzero, unless is_same_v<decay_t<T>, variant> is false, unless decay_t<T> is neither a specialization of in_place_type_t nor a specialization of in_place_index_t, unless is_constructible_v<Tj, T> is true, and unless the expression FUN(std::forward(t)) (with FUN being the above-mentioned set of imaginary functions) is well formed.
So std::variant v2 = v1; fails in compiler versions that do not take into account the added clause (like GCC 7.1. See DEMO) but succeeds on later versions (From GCC 7.2 onwards. See DEMO).
Code looks like:
struct Foo {
Foo(const char *);
};
Foo::Foo(const char *str = 0)
{
}
VS 2013 and gcc 4.8.0 accept such code,
while clang 3.3 reject such code with:
error: addition of default argument on redeclaration makes this constructor a default constructor
who is right from standard (C++03 and C++11) point of view?
Note:
I like clang's choice too, but I going to report bug to gcc and visual studio,
and if this is not correct from standard point of view, this helps to
convince compiler's developers to fix this issue.
GCC
I described issue here: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=58194
But no luck, they suspend bug fixing untill draft become standard.
This has been discussed on the Clang mailinglist and has been submitted as a Defect Report Core Issue 1344.
From the mailinglist discussion:
The idea is that the presence of certain special members affects core
properties of a class type, like whether it's POD or trivially
copyable. Deciding these properties should not require whole-program
knowledge; it's important for us to be able to deduce them just from
the class definition. The really problematic case is turning a
"normal" constructor into a copy or move constructor by adding default
arguments, but IIRC introducing a default constructor was also
problematic.
The fix is that you should put the default argument in the initial
declaration of the constructor.
This was last discussed by WG21 at the Bloomington meeting. Notes from
there:
"Consensus: Make this ill-formed as suggested in the write-up. Core
issue 1344. Priority 0, Doug drafting."
So CWG has agreed (in principle) that this should be ill-formed.
TL;DR Clang is right whenever the defect gets fixed (not sure if that can officially only happen with C++14, or if such Committee decisions can also be done retroactively on C++11)
I would say CLANG is right. The standard says (12.1.5 for the both old and new versions of the standard):
A default constructor for a class X is a constructor of class X that can be called without an argument
Adding the default value to the only argument of the constructor definitely makes it possible to call it without arguments, thus making it a default one. Also, 8.3.6 says (emphasis mine):
A default argument expression shall be specified only in the
parameter-declaration-clause
of a function declaration <...>
You have a declaration and a definition. In your declaration you do not have a default value, while in your definition you have a default value. In fact the signature of the declaration is very similar to the signature of the definition, but not the same. I believe that strictness is a good idea, so I believe it is better to enforce that the declaration is the same as the definition.