Continuing something learned in C++ error: base function is protected ...
The C++11 pointer-to-member rules effectively strip the protected
keyword of any value, because protected members can be accessed in unrelated classes without any evil/unsafe casts.
To wit:
class Encapsulator
{
protected:
int i;
public:
Encapsulator(int v) : i(v) {}
};
Encapsulator f(int x) { return x + 2; }
#include <iostream>
int main(void)
{
Encapsulator e = f(7);
// forbidden: std::cout << e.i << std::endl; because i is protected
// forbidden: int Encapsulator::*pi = &Encapsulator::i; because i is protected
// forbidden: struct Gimme : Encapsulator { static int read(Encapsulator& o) { return o.i; } };
// loophole:
struct Gimme : Encapsulator { static int Encapsulator::* it() { return &Gimme::i; } };
int Encapsulator::*pi = Gimme::it();
std::cout << e.*pi << std::endl;
}
Is this really conformant behavior according to the Standard?
(I consider this a defect, and claim the type of &Gimme::i
really should be int Gimme::*
even though i
is a member of the base class. But I don't see anything in the Standard that makes it so, and there's a very specific example showing this.)
I realize some people may be surprised that the third commented approach (second ideone test case) actually fails. That's because the correct way to think about protected is not "my derived classes have access and no one else" but "if you derive from me, you will have access to these inherited variables contained in your instances, and no one else will unless you grant it". For example, if Button
inherits Control
, then protected members of Control
within a Button
instance are accessible only to Control
, and Button
, and (assuming Button
doesn't prohibit it) the actual dynamic type of the instance and any intervening bases.
This loophole subverts that contract, and completely opposed the spirit of the rule 11.4p1:
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. 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 denoteC
or a class derived fromC
. All other accesses involve a (possibly implicit) object expression. In this case, the class of the object expression shall beC
or a class derived fromC
.
Thanks to AndreyT for linking http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#203 which provides additional examples motivating a change, and calls for the issue to be brought up by the Evolution Working Group.
Also relevant: GotW 76: Uses and Abuses of Access Rights
The main thing to keep in mind about access specifiers in C++ is that they control where a name can be used. It does not actually do anything to control access to objects. "access to a member" in the context of C++ means "the ability to use a name".
Observe:
This,
e.*&Gimme::i
, is allowed because it does not access a protected member at all. We are accessing the member created insideGimme
by theusing
declaration. That is, even though ausing
declaration does not imply any additional sub-objects inGimme
instances, it still creates an additional member. Members and sub-objects are not the same thing, andGimmie::i
is a distinct public member that can be used to access the same sub-objects as the protected memberEncapsulator::i
.Once the distinction between 'member of a class' and 'sub-object' is understood it should be clear that this is not actually a loophole or unintended failure of the contract specified by 11.4 p1.
That one can create an accessible name for, or otherwise provide access to, an otherwise un-nameable object is the intended behavior even though it is different from some other languages and may be surprising.
I have seen this technique, that I refer to as "protected hack", mentioned quite a few times here and elsewhere. Yes, this behavior is correct and it is indeed a legal way to circumvent protected access without resorting to any "dirty" hacks.
When
m
is member of classBase
, then the problem with making the&Derived::m
expression to produce a pointer ofDerived::*
type is that class member pointers are contravariant, not covariant. It would make the resultant pointers unusable withBase
objects. For example, this code compilesbecause
&Derived::m
produces anint Base::*
value. If it produced aint Derived::*
value, the code would fail to compile at line 1. And if we attempted to fix it withit would fail to compile at line 2. The only way to make it compile would be to perform a forceful cast
which is not good.
P.S. I agree, this is not a very convincing example ("just use
&Base:m
from the beginning and the problem is solved"). However, http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#203 has more info that sheds some light on why such decision was made originally. They state