This is nothing more than a bug in that particular version of GCC.
C++03 [class.copy]/10 governs the signature of the implicitly declared copy assignment operator for a class. For a class named X
, it is either X& operator=(const X&)
(most common) or X& operator=(X&)
(less common). In the case of A
, the copy assignment operator's signature is A& operator=(const A&)
. Note that this means that you cannot call this assignment operator with a volatile or const volatile value of type A
.
Similarly, the implicitly declared assignment operator for U
will have the signature U& operator=(const U&)
. [class.copy]/13 explains how this implicitly declared assignment operator works:
The implicitly-defined copy assignment operator for class X
performs memberwise assignment of its subobjects. The direct base classes of X
are assigned first, in the order of their declaration in the base-specifier-
list, and then the immediate nonstatic data members of X
are assigned, in the order in which they were declared in the class definition. Each subobject is assigned in the manner appropriate to its type:
- if the subobject is of class type, the copy assignment operator for the class is used (as if by explicit qualification; that is, ignoring any possible virtual overriding functions in more derived classes);
- if the subobject is an array, each element is assigned, in the manner appropriate to the element type;
- if the subobject is of scalar type, the built-in assignment operator is used.
It is unspecified whether subobjects representing virtual base classes are assigned more than once by the
implicitly-defined copy assignment operator. [...]
Thus, U::operator=
behaves as though it were implemented like so:
U& operator=(const U& other) {
a = other.a;
b = other.b;
return *this;
}
And a = other.a
is ill-formed, because other.a
has type volatile A
, and A
doesn't have an assignment operator that can accept volatile A
.
That particular version of GCC probably accepted it because it converted the assignment into a trivial copy operation (i.e. as if by memcpy
). It is allowed to do that, but only if the semantic requirements for the assignment are met in the first place. It is not allowed to fail to diagnose a diagnosable error because it decided to perform a particular optimization.
[over.match.oper]/1 makes it clear that if at least one operand for an operator has class type, then overload resolution must be done. In an assignment expression of the form
a = other.a
both operands have class type. [over.match.oper]/3 then explains that the candidates consist of the member candidates, non-member candidates, and built-in candidates. Non-member operator=
is permitted, so the second set is empty. The member candidate is A::operator=(const A&);
. The list of possible built-in candidates is given in [over.built], but there is no built-in candidate whose left argument is a reference to class type, and [over.match.oper]/4 forbids attempting to perform a user-defined conversion on the left argument in order to convert it to something that a built-in operator=
candidate can accept. So the overload resolution rules preclude any possibility that assigning to an A
object can be interpreted as a use of a built-in operator. It always must call an assignment operator function, and as I explained previously, the assignment operator for A
cannot accept a volatile argument. The compiler must diagnose this.
union U
:volatile U& operator =(const volatile U&) volatile
? Or perhaps with only some of thosevolatile
s? – Alenaalene