Short answer: There is no undefined behavior involved. The behavior you see is:
- The expression
&A::a
is an attempt to obtain a pointer to member pointing to member a of class A. If a is protected in A, this expression only passes access checks within class A (or friends of A). In a class B derived from A, you can get the same pointer to member only via the expression &B::a
(note that the type of this expression will still be int A::*
). So:
- if
A::a
is protected in A, the expression &A::a
is not allowed in a member function of derived class B. This is your compiler error.
- if
A::a
is public in A, this expression is valid, producing a pointer to memeber.
- Streaming a pointer to member to an
ostream
, for example using cout << &A::a
will print 1
. This results from invoking ostream::operator << (bool)
. You can use the boolalpha manipulator to see that this is indeed the chosen overload: cout << boolalpha << &A::a
will print true
.
- If you use the modified expression &(A::a) or simply &a, no pointer to member is formed. Here the address of member a of the current object (i.e the same as
&(this->a)
) is taken, which is a regular pointer to int. This access to a protected member of a base class subobject of *this
is valid, so that variant can be used even if a is protected in A.
Longer explanation:
The standard says (5.3.1/3):
The result of the unary & operator is a pointer to its operand. The
operand shall be an lvalue or a qualified- id. If the operand is a
qualified-id naming a non-static member m of some class C with type T,
the result has type “pointer to member of class C of type T” and is a
prvalue designating C::m. [...]
So the expression &A::a
attempts to obtain a pointer-to-member to member a of class A.
In the next paragraph (5.3.1/4), it is elaborated that only the &X::m syntax produces a pointer to member - neither &(X::m)
, nor &m
or plain X::m
do:
A pointer to member is only formed when an explicit & is used and its
operand is a qualified-id not enclosed in parentheses.
But such an expression is only valid, if access is allowed. In case of a protected member (11.4/1) applies:
An additional access check beyond those described earlier in Clause 11
is applied when a non-static data member or non-static member function
is a protected member of its naming class (11.2) As described
earlier, access to a protected member is granted because the reference
occurs in a friend or member of some class C. If the access is to form
a pointer to member (5.3.1), the nested-name-specifier shall denote C
or a class derived from C. [...]
In your case access to the protected member a would be granted, because the reference to a occurs in a member of class B, derived from A. As the expression attempts to form a pointer to member, the nested-name-specifier (the part before the final "::a") must denote B. Thus the simplest allowed form is &B::a
. The form &A::a
is only allowed within members or friends of class A itself.
There is no formatted output operator for pointers to member (neither as istream member nor as free operator function), so the compiler will look at overloads that can be called using a standard conversion (sequence). The only standard conversion from pointers to member to something else is described in 4.12/1:
A prvalue of [...] pointer to member type can be converted to a
prvalue of type bool. A [...] null member pointer value is converted
to false; any other value is converted to true. [...]
This conversion can be used without additional conversions to call basic_ostream<charT,traits>& basic_ostream<charT,traits>::operator<<(bool n)
. Other overloads require longer conversion sequences, so that overload is the best match.
As &A::a
takes the address of some member, it is not a null member pointer value. Thus it will convert to true
, which prints as "1" (noboolalpha) or "true" (boolalpha).
Finally, the expression &(A::a)
is valid in a member of B, even if a is protected in A. by the above rules this expression does not form a pointer to member, so the special access rule quoted above does not apply. For such cases 11.4/1 continues:
All other accesses involve a (possibly implicit) object expression
(5.2.5). In this case, the class of the object expression shall be C
or a class derived from C.
Here the object impression is an implicit (*this)
, i.e. A::a
means the same as (*this).A::a
. The type of (*this)
obviously is the same as the class where the access occurs (B), so the access is allowed. [Note: int x = A(42).a
would not be allowed within B.]
So &(A::a)
within B::show()
means the same as &(this->a)
and that is a plain pointer to int.