Declaring defaulted assignment operator as constexpr: which compiler is right? - c++

Consider
struct A1 {
constexpr A1& operator=(const A1&) = default;
~A1() {}
};
struct A2 {
constexpr A2& operator=(const A2&) = default;
~A2() = default;
};
struct A3 {
~A3() = default;
constexpr A3& operator=(const A3&) = default;
};
GCC and MSVC accept all three structs. Clang rejects A1 and A2 (but accepts A3), with the following error message:
<source>:2:5: error: defaulted definition of copy assignment operator is not constexpr
constexpr A1& operator=(const A1&) = default;
^
<source>:6:5: error: defaulted definition of copy assignment operator is not constexpr
constexpr A2& operator=(const A2&) = default;
^
2 errors generated.
(live demo)
Which compiler is correct, and why?

I think all three compilers are wrong.
[dcl.fct.def.default]/3 says:
An explicitly-defaulted function that is not defined as deleted may be declared constexpr or consteval only if it would have been implicitly declared as constexpr. If a function is explicitly defaulted on its first declaration, it is implicitly considered to be constexpr if the implicit declaration would be.
When is the copy assignment operator implicitly declared constexpr? [class.copy.assign]/10:
The implicitly-defined copy/move assignment operator is constexpr if
X is a literal type, and
[...]
Where a literal type is, from [basic.types]/10:
A type is a literal type if it is:
[...]
a possibly cv-qualified class type that has all of the following properties:
it has a trivial destructor,
[...]
A1 doesn't have a trivial destructor, so its implicit copy assignment operator isn't constexpr. Hence that copy assignment operator is ill-formed (gcc and msvc bug to accept).
The other two are fine, and it's a clang bug to reject A2.
Note the last bit of [dcl.fct.def.default] that I quoted. You don't actually have to add constexpr if you're explicitly defaulting. It would be implicitly constexpr where that is possible.

The C++17 standard states:
15.8.2 Copy/move assignment operator [class.copy.assign]
...
10 A copy/move assignment operator for a class X that is defaulted and not defined as deleted is implicitly defined when it is odr-used (6.2) (e.g., when it is selected by overload resolution to assign to an object of its class type) or when it is explicitly defaulted after its first declaration. The implicitly-defined copy/move assignment operator is constexpr if
(10.1) — X is a literal type, and
(10.2) — the assignment operator selected to copy/move each direct base class subobject is a constexpr function, and
(10.3) — for each non-static data member of X that is of class type (or array thereof), the assignment operator selected to copy/move that member is a constexpr function.
The copy-assignment operator satisfies the above requirements in two of the cases. In the first case, we have a non-literal type because of the non-trivial destructor.
So I believe Clang is wrong to reject the code in the second case.
There is a bug filed with Clang titled: Defaulted destructor prevents using constexpr on defaulted copy/move-operator which shows the same symptoms as the code in the OP.
The comments from the bug report state:
When defaulted destructor is commented out (i.e. not user declared), then errors cease to exist.
and
The problem also goes away if you declare the destructor before the copy assignment operator.
This is true of the code in the question as well.
As #YSC points out, another relevant quote here is:[dcl.fct.def.default]/3 which states:
An explicitly-defaulted function that is not defined as deleted may be declared constexpr or consteval only if it would have been implicitly declared as constexpr. If a function is explicitly defaulted on its first declaration, it is implicitly considered to be constexpr if the implicit declaration would be.

Related

Why is copy assigment possible, if a class has only a (templated) move assignment operator?

I have stumbled over code today, that I don't understand. Please consider the following example:
#include <iostream>
#include <string>
class A
{
public:
template <class Type>
Type& operator=(Type&& theOther)
{
text = std::forward<Type>(theOther).text;
return *this;
}
private:
std::string text;
};
class B
{
public:
B& operator=(B&& theOther)
{
text = std::forward<B>(theOther).text;
return *this;
}
private:
std::string text;
};
int main()
{
A a1;
A a2;
a2 = a1;
B b1;
B b2;
b2 = b1;
return 0;
}
When compiling, MinGW-w64/g++ 10.2 states:
..\src\Main.cpp: In function 'int main()':
..\src\Main.cpp:41:7: error: use of deleted function 'B& B::operator=(const B&)'
41 | b2 = b1;
| ^~
..\src\Main.cpp:19:7: note: 'B& B::operator=(const B&)' is implicitly declared as deleted because 'B' declares a move constructor or move assignment operator
19 | class B
| ^
mingw32-make: *** [Makefile:419: Main.o] Error 1
I fully understand the error message. But I don't understand why I don't get the same message with class A. Isn't the templated move assignment operator also a move assignment operator? Why then is the copy assignment operator not deleted? Is this well-written code?
Isn't the templated move assignment operator also a move assignment operator?
No, it's not considered as move assignment operator.
(emphasis mine)
A move assignment operator of class T is a non-template non-static member function with the name operator= that takes exactly one parameter of type T&&, const T&&, volatile T&&, or const volatile T&&.
As the effect, A still has the implicitly-declared copy/move assignment operator.
BTW: Your template assignment operator takes forwarding reference, it could accept both lvalue and rvalue. In a2 = a1;, it wins against the generated copy assignment operator in overload resolution and gets called.
As complement to #songyuanyao's standard-based answer: these kind of language rules are common reasons for guidelines such as MISRA/AUTOSAR (language guidelines for safety-critical C++ development) to have "avoid developer confusion" rules such as:
(from AUTOSAR C++14 Guidelines)
Rule A14-5-1 (required, implementation, automated)
A template constructor shall not participate in overload resolution
for a single argument of the enclosing class type.
Rationale
A template constructor is never a copy or move constructor and
therefore doesn’t prevent the implicit definition of a copy or move
constructor even if the template constructor looks similar and might
easily be confused. At the same time, copy or move operations do not
necessarily only use a copy or move constructor, but go through the
normal overload resolution process to find the best matching function
to use. This can cause confusion in the following cases:
a template constructor that looks like a copy/move constructor is not selected
for a copy/move operation because the compiler has generated an implicit copy/move constructor as well a template constructor is
selected in preference over a copy/move constructor because the
template constructor is a better match
To avoid these confusing situations, template constructors shall not
participate in overload resolution for a single argument of the
enclosing class type to avoid a template constructor being selected
for a copy/move operation. It also makes it clear that the constructor
is not a copy/move constructor and that it does not prevent the
implicit generation of copy/move constructors.
Rule M14-5-3 (required, implementation, automated)
A copy assignment operator shall be declared when there is a template
assignment operator with a parameter that is a generic parameter.
Namely that it can be surprising for developers that a template copy/move ctor/assignment operator does not suppress (/does not "activate" rule of 5) implicitly-generated ones. You are typically required to use SFINAE to make sure that the templated ctor/assignment op does not act as if it was a copy/move ctor/assignment by allowing the overload to be active for a single argument of the enclosing class.

=default in declaration vs definition

I know that instead of writing:
class A {
public:
A(A&&) noexcept = default;
};
One should better write
class A {
public:
A(A&&) noexcept;
};
inline A::A(A&&) noexcept = default;
The reasons I've heard are:
It avoids the constructor becomes deleted. Compiler will give an error if it is unable to define the function.
The move constructor is declared noexcept even if some of the member fields' move constructor are not annotated with noexcept.
Could someone explain a bit more about the theory behind the differences?
Only declaration is used to describe the class/method, so when doing
class A {
public:
A(A&&) noexcept;
};
You might even implement A::A(A&&) as you want (definition can be in different TU)
When you implement it with:
A::A(A&&) noexcept = default;
Compiler has to generate the method (it cannot tell if it is implicitly deleted as declaration precise method exists), and provides diagnostic if it can't.
But when you declare it inside the class:
class A {
public:
A(A&&) noexcept = default;
};
It is "part" of declaration. so it might be implicitly deleted (because of member or base class).
Same apply for noexcept.
An other advantage to put definition in dedicated TU, it that definition of required dependencies can be only in that TU, instead of each place where the method would be generated. (Useful for pimpl idiom for example).
One disadvantage of split definition and declaration is that the method is now "user provided", that may affect traits as trivially_constructible/copyable/...
The behavior is covered in [dcl.fct.def.default]p3 which says:
If a function that is explicitly defaulted is declared with a noexcept-specifier that does not produce the same
exception specification as the implicit declaration (18.4), then
(3.1) — if the function is explicitly defaulted on its first declaration, it is defined as deleted;
(3.2) — otherwise, the program is ill-formed.
Note the wording changes in C++20 but the intent is the same for this case. I find the C++17 wording simpler to grok.
For example given:
struct S {
S( S&& ) noexcept(false) = default;
};
The move constructor is defined as deleted since due to [except.spec]p7:
An implicitly-declared constructor for a class X, or a constructor without a noexcept-specifier that is defaulted
on its first declaration, has a potentially-throwing exception specification if and only if any of the following
constructs is potentially-throwing:
(7.1) — a constructor selected by overload resolution in the implicit definition of the constructor for class X to
initialize a potentially constructed subobject, or
(7.2) — a subexpression of such an initialization, such as a default argument expression, or,
(7.3) — for a default constructor, a default member initializer.
none of the cases hold.
If we got back to [dcl.fct.def.default]p3 it says otherwise the program is ill-formed. Ill-formed programs require a diagnostic so if we modify the first example as follows (see it live):
struct S {
S( S&& ) noexcept(false) ;
private:
int i;
};
S::S( S&&) noexcept(false) = default ;
it will produce a diagnostic e.g.:
error: function 'S::S(S&&)' defaulted on its redeclaration with an exception-specification that differs from the implicit exception-specification 'noexcept'
S::S( S&&) noexcept(false) = default ;
^
Note clang bug related to this case, it seems they are not following Defect Report 1778.
You may want to note Declaring a function as defaulted after its first declaration which covers some the optimization/interface issues.

Why is it impossible to explicitly default a copy constructor with volatile argument?

I could not find where in the standard it is stated that it is forbidden to explicitly default a copy-constructor and copy-assignment with a volatile& or const volatile& argument, like this:
struct A{
A(const volatile A&) =default; // fails to compile on (all) compilers
};
In [dcl.fct.def.default] there is no such a restriction, while [class.copy] specifies that A(const volatile A&) is a copy constructor.
Note: I am just looking for the location in the text of the standard which specifies this behavior.
You are in the right sections, but are overlooking some crucial bullets.
[dcl.fct.def.default]/1:
A function definition of the form:
...
is called an explicitly-defaulted definition. A function that is
explicitly defaulted shall
have the same declared function type (except for possibly differing ref-qualifiers and except that in the case of a copy
constructor or copy assignment operator, the parameter type may be
“reference to non-const T”, where T is the name of the member
function's class) as if it had been implicitly declared, and
[class.copy.ctor]/7:
The implicitly-declared copy constructor for a class X will have the
form
X::X(const X&)
if each potentially constructed subobject of a class type M (or array
thereof) has a copy constructor whose first parameter is of type const
M& or const volatile M&.119 Otherwise, the implicitly-declared copy
constructor will have the form
X::X(X&)
...
119) This implies that the reference parameter of the implicitly-declared copy constructor cannot bind to a volatile lvalue;
When the above is summed up, your only two options for explicitly defaulting a copy c'tor are these:
struct A {
A(const A&) = default;
};
struct B {
B(B&) = default;
};
When the standard says A(const volatile A&) is a copy constructor. It means that a user-provided c'tor with such a parameter can be the classes copy c'tor.

Default move constructor taking a const parameter

When defining a class, is the following valid?
T(const T&&) = default;
I was reading about move constructors here and it explains how the default may still be implicitly declared:
A class can have multiple move constructors, e.g. both T::T(const T&&)
and T::T(T&&). If some user-defined move constructors are present, the
user may still force the generation of the implicitly declared move
constructor with the keyword default.
At the bottom of the page it mentions defect report CWG 2171:
CWG 2171 C++14
X(const X&&) = default was non-trivial, made trivial.
Maybe the wiki entry just has a mistake and CWG 2171 is only referring to a copy constructor, not a move constructor?
From the n4296 draft:
8.4.2.1 Explicitly-defaulted functions:
A function that is explicitly defaulted shall
(1.1) — be a special member function,
(1.2) — have the same declared function type (except for possibly
differing ref-qualifiers and except that in the case of a copy
constructor or copy assignment operator, the parameter type may be
“reference to non-const T”, where T is the name of the member
function’s class) as if it had been implicitly declared, and
(1.3) — not have default arguments.
12.8.10 Copying and moving class objects:
The implicitly-declared move constructor for class X will have the form
X::X(X&&)
As a consequence the line:
T(const T&&) = default;
is not valid because the implicitly-declared move constructor has the form of:
T(T&&)

Can I add an implicit conversion from a volatile T to a T?

This code
struct T {
int m_x;
T(int x) : m_x(x) {}
operator T() {
return T(0);
}
};
int main() {
volatile T v(2);
T nv(1);
nv = v; // nv.m_x = 0
}
Gives:
prog.cpp: In function ‘int main()’:
prog.cpp:14:10: error: no match for ‘operator=’ in ‘nv = v’
prog.cpp:14:10: note: candidates are:
prog.cpp:1:8: note: T& T::operator=(const T&)
prog.cpp:1:8: note: no known conversion for argument 1 from ‘volatile T’ to ‘const T&’
prog.cpp:1:8: note: T& T::operator=(T&&)
prog.cpp:1:8: note: no known conversion for argument 1 from ‘volatile T’ to ‘T&&’
What typecast overload do I need to define for this to work?
The short answer:
Yes you can but the compiler won't do the job for you.
You cannot have an compiler-provided conversion from volatile T to T but a user-defined implicit conversion using a volatile-qualified constructor.
It is also impossible to declare such a conversion by using explicitly-defaulted versions of the special member functions (see long answer for reference).
You'll have to provide a user-defined way of conversion to enable such assignments. You can either
use a non-explicit copy constructor with a cv-qualified argument for implicit user-defined conversion or
a copy assignment operator taking a v-qualified argument.
Example:
X (X const volatile & xo);
X& operator= (X const volatile & xo);
The long answer with standard quotes 'n stuff or
why doesn't the compiler do this for me?
Way 1: User-provided constructor from volatile T
Standard, ISO 14882:2011, 4/3
An expression e can be implicitly converted to a type T if and only if the declaration T t=e; is well-formed, for some invented temporary variable t (8.5).
Since the declaration T t = e;, where in this case e is typed volatile T, requires such a copy-initialization to be valid, you'll need a copy constructor from a volatile T.
I already answered (Why am I not provided with a default copy constructor from a volatile?).
Therefore you'll need to provide a user-defined way of copy-initialization of T from volatile T.
X (X const volatile & xo);
Note:
This is a declaration, you'll also have to provide a definition.
The constructor must not be explicit.
Providing an user-defined copy constructor taking a volatile argument will result in the abscence of an implicitly generated default assignment operator.
This will make your assignment work.
Way 2: User-provided copy assignment operator from volatile T
Another way to make the assignment of your example code work is a copy assignment operator.
Unfortunatelly, the standard also does say that a compiler will not provide implicit copy assignment operators for the conversion of volatile to non volatile objects.
Standard, ISO 14882:2011, 12.8/18
If the class definition does not explicitly declare a copy assignment operator, one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy assignment operator is defined as deleted; otherwise, it is defined as defaulted (8.4). The latter case is deprecated if the class has a user-declared copy constructor or a user-declared destructor. The implicitly declared copy assignment operator for a class X will have the form
X& X::operator=(const X&)
if
each direct base class B of X has a copy assignment operator whose parameter is of type const B&, const volatile B& or B, and
for all the non-static data members of X that are of a class type M (or array thereof), each such class type has a copy assignment operator whose parameter is of type const M&, const volatile M& or M. 122
Otherwise, the implicitly-declared copy assignment operator will have the form
X& X::operator=(X&)
Note 122 on 12.8/18
the reference parameter of the implicitly-declared copy assignment operator cannot bind to a volatile lvalue; see C.1.9.
In the other answer I quoted C.1.9 where it says:
The implicitly-declared copy constructor and implicitly-declared copy assignment operator cannot make a copy of a volatile lvalue. [ ... ]
The result is that we'll have to provide a suitable copy assignment operator if we want to have one.
X& operator= (X const volatile & xo);
Also note that you cannot declare an assignment/constructor from volatile explicitly-defaulted.
C++11 Standard 8.4.2/1
A function that is explicitly defaulted shall
be a special member function,
have the same declared function type (except for possibly differing ref-qualifiers and except that in the case of a copy constructor or copy assignment operator, the parameter type may be “reference to non-const T”, where T is the name of the member function’s class) as if it had been implicitly declared, and
not have default arguments.
The following Note was removed from the final C++11 standard but was present in the Draft N3242. It still holds.
[ Note: This implies that parameter types, return type, and cv-qualifiers must match the hypothetical implicit declaration. —end note ]
Since the hypothetical implicit declaration is non-volatile you cannot have the defaulted.
Here's how you get a copy constructor and copy assignment that allows a volatile source:
struct X {
X(const X& o) : members(o.members) {}
X(const volatile X& o) : members(o.members) {}
X& operator=(const X& o) {v=o.v; return *this;}
X& operator=(const volatile X& o) {v=o.v; return *this;}
};
Note though that this has some consequences. The type is no longer POD or even trivially copyable, for one. Which might defeat the whole point of making it volatile.
You can implement the assignment operator =:
T& operator=(const volatile T &rhs) {
m_x = rhs.m_x;
return *this;
}