Question

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 denote C or a class derived from C. All other accesses involve a (possibly implicit) object expression. In this case, the class of the object expression shall be C or a class derived from C.


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

Was it helpful?

Solution

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 class Base, then the problem with making the &Derived::m expression to produce a pointer of Derived::* type is that class member pointers are contravariant, not covariant. It would make the resultant pointers unusable with Base objects. For example, this code compiles

struct Base { int m; };
struct Derived : Base {};

int main() {
  int Base::*p = &Derived::m; // <- 1
  Base b;
  b.*p = 42;                  // <- 2
}

because &Derived::m produces an int Base::* value. If it produced a int Derived::* value, the code would fail to compile at line 1. And if we attempted to fix it with

  int Derived::*p = &Derived::m; // <- 1

it would fail to compile at line 2. The only way to make it compile would be to perform a forceful cast

  b.*static_cast<int Base::*>(p) = 42; // <- 2

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

Notes from 04/00 meeting:

The rationale for the current treatment is to permit the widest possible use to be made of a given address-of-member expression. Since a pointer-to-base-member can be implicitly converted to a pointer-to-derived-member, making the type of the expression a pointer-to-base-member allows the result to initialize or be assigned to either a pointer-to-base-member or a pointer-to-derived-member. Accepting this proposal would allow only the latter use.

OTHER TIPS

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:

class Encapsulator {
  protected:
    int i;
};

struct Gimme : Encapsulator {
    using Encapsulator::i;
};

int main() {
  Encapsulator e;
  std::cout << e.*&Gimme::i << '\n';
}

This, e.*&Gimme::i, is allowed because it does not access a protected member at all. We are accessing the member created inside Gimme by the using declaration. That is, even though a using declaration does not imply any additional sub-objects in Gimme instances, it still creates an additional member. Members and sub-objects are not the same thing, and Gimmie::i is a distinct public member that can be used to access the same sub-objects as the protected member Encapsulator::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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top