2

Consider

class A {
  protected:
    int m;
};
class B : public A {
    void foo(A& a) {
        a.m = 42;  // ill-formed
    }
    void bar(A& a) {
        auto pm = &B::m;
        auto pm2 = static_cast<int A::*>(pm);
        a.*pm2 = 42;  // is this ok?
    }
};

Trying to access A::m directly is ill-formed according to [class.protected]. However, it appears that we can always (?) circumvent this using static_cast, which allows a derived-to-base cast with pointers to members. Or is this somehow UB?

[Coliru link showing that bar compiles]

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • It is OK even without the explicit cast. – xskxzr Feb 15 '19 at 05:21
  • Since `bar` and `B` have access to `m` it is accessible. As long as `A` is not virtual and the name lookup is not ambiguous this is perfectly fine. – rmm19433 Feb 15 '19 at 05:24
  • @rmm19433: It's not perfectly fine. `B` member functions should have access only to members of `B` object instances (which is why the direct access fails). The language rules do allow it despite not being perfectly fine (most `static_cast` scenarios are not type-safe, the programmer is overriding the compiler's protections). – Ben Voigt Feb 15 '19 at 05:51
  • @BenVoigt you can omit the `static_cast`. `a.*(&B::m) = 42;` works as well – rmm19433 Feb 15 '19 at 05:57
  • @rmm19433: Indeed, the language rules give `&B::m` the wrong type -- it's `int A::*` when it should have been `int B::*`. But it's still a roundabout way of confusing the compiler into skipping the access check. The compiler knows that `m` should only be accessible on instances of `B`, and when you try `a.m` it tells you that. – Ben Voigt Feb 15 '19 at 06:00
  • 1
    @BenVoigt No the type is correct. `B::m` designates an object in `A`, it is an alias to `A::m`. To get `&B::m` to have type `int B::*` you would have to concoct some arcane rules to specify when and why certain aliases are somehow not really aliases that have really specifically different semantics in this regard. – Passer By Feb 15 '19 at 06:11
  • @PasserBy: Exactly the opposite. The grammar construct for forming a pointer to member is already an extreme special case. `B::m` does not designate an object in the type `A`, it designates a member of `*this` (specifically in the `A` subobject of `*this`, but still, the one in `*this`). `&(B::m)` has type `int*` not `int A::*`. Only when `&` and `::` are combined do you get a pointer-to-member, and right now the arcane rules for qualified lookup have had to be duplicated into the pointer-to-member-formation rules. Having `int B::*` as the type would cut out rules, not add them. – Ben Voigt Feb 15 '19 at 06:19
  • @BenVoigt I'm not sure what your point is about `*this`, sure it is an member of a subobject, but it is still not a direct member of `B`. The qualified lookup rules _aren't_ duplicated, specifically [expr.unary](https://timsong-cpp.github.io/cppwp/expr.unary#op-3) makes no mention of lookup. The standard [repeatedly](https://timsong-cpp.github.io/cppwp/class.member.lookup#6) [uses](https://timsong-cpp.github.io/cppwp/class.qual#1.sentence-2) the notion "members of base classes", it never did consider members of base classes the members of the class itself. BTW `&(B::m)` is illegal. – Passer By Feb 15 '19 at 07:45

1 Answers1

4

Yes, you can circumvent the protected mechanism in this way by using static_cast.

I think this is not undefined behavior in this particular case.

By using the static_cast you tell the compiler two things:

  1. You ask the compiler to convert the B pointer into an A pointer.

  2. You tell the compiler that this is ok to do so.

For 1. the compiler applies very limited checks of whether this is okay or not, and for static_cast it allows to cast from derived to base and the other way round, and that is it. So the compiler is happy. Whether a variable or pointer is protected or public is not part of the variable or pointer type. Neither pm nor pm2 carry the protected information.

For 2. the compiler completely leaves it up to you to make the decision whether this is okay or not in your design. It is not undefined behavior. It may still not be a good idea. pm2 is just a pointer to an int in A. You can reset it to a pointer to a different int in A which is public.

The background is that access control in C++ is generally per-class, plus there are some extra rules around protected which try to provide some level of access control on a per instance basis, but this protection is not perfect as you have demonstrated in your interesting question.

P.W
  • 26,289
  • 6
  • 39
  • 76
Johannes Overmann
  • 4,914
  • 22
  • 38