Topic: Circumventing the restrictions of "protected" via pointers to members (DR?)


Author: Pavel Minaev <int19h@gmail.com>
Date: Tue, 24 Nov 2009 12:46:47 CST
Raw View
C++03 defines "protected" accessibility for members in 11.5
[class.protected]/1 thus:

"Except when forming a pointer to member (5.3.1), the access must be
through a pointer to, reference to, or object of the derived class
itself (or any class derived from that class) (5.2.5). If the access
is to form a pointer to member, the nested-name-specifier shall name
the derived class (or any class derived from that class)."

The first part regarding access in non-pointer-to-member case is
perfectly clear. It's why we can't write:

   struct base {
       protected: int x
   };

   struct derived : base {
       void foo(base* b) { b->x = 123; } // not ok
   };

The rationale is clear, too. Now the part about pointers to members
confuses me. Applied to the above code, it means that to get pointer
to b, we have to write &derived::x, and are forbidden from writing
&base::x. I have always thought that this restriction is there for the
same reason as the first one, and that the type of &derived::x would
be a pointer-to-member-of-derived, thus preserving the strictness of
"protected". But to my surprise, I found out recently that this isn't
the case. Again, the Standard, 5.3.1[expr.unary.op]/2:

"For a qualified-id, if the member is a static member of type    T   , the
type of the result is plain    pointer to T.    If the member is a
nonstatic member of class C of type T, the type of the result is
   pointer to member of class C of type
T.    [Example:
struct A { int i; };
struct B : A { };
... &B::i ... // has type int A::*"

So this means that type of &derived::x is still (int base::*) - and
that consequently means that I can then use that member pointer with
any object pointer of type base* - even if it actually points at some
other class, say, derived2, which does not inherit derived - and
perform the very same "cross-hierarchy" protected access that the
definition of "protected" is so carefully written to prohibit!

   struct base {
       protected: int x
   };

   struct derived2 : base { };

   struct derived : base {
       void foo(base* b) {
          b->x = 123; // not ok
          (b->*(&derived::x)) = 123; // ok?!
       }
   };

In fact, trying that on MSVC, g++ and Comeau, all of them agree that
the above is perfectly fine and dandy (while &base::x does not
compile).

Is that a known problem? Is that even a problem (I find it hard to
believe otherwise, but still...)? I didn't spot anything in active or
closed issues or DRs, nor any changes to this in the most recent C++0x
draft. A quick Google search didn't give any hits exploring this
particular corner case, either.


One other thing of note is that is seems to be even more convoluted
when virtual functions are involved. Consider this iteration with
pointers to member functions:

   struct base {
       protected: virtual void foo() {}
   };

   struct derived2 : base { };

   struct derived : base {
       void bar(base* b) {
          b->foo(); // not ok
          (b->*(&derived::foo))(); // ok?!
       }
   };

as before, this works, and for the same reason. But if I override foo
in derived:

   struct base {
       protected: virtual void foo() {}
   };

   struct derived2 : base { };

   struct derived : base {
       virtual void foo() {}
       void bar(base* b) {
          b->foo(); // not ok
          (b->*(&derived::foo))(); // not ok anymore, either!
       }
   };

Now the type of &derived::foo is a pointer to member of derived, and
is not compatible with type of b (and no casts are available), so the
loophole isn't there. If the loophole is deliberate, it seems to be
very inconsistent - since member pointer dispatch is virtual at the
point of the call, not at the point when the pointer is obtained, why
should virtualness of function, and presence of overrides, make any
difference either way?


--
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html                      ]





Author: "Johannes Schaub (litb)" <schaub-johannes@web.de>
Date: Tue, 24 Nov 2009 14:37:45 CST
Raw View
Pavel Minaev wrote:

> C++03 defines "protected" accessibility for members in 11.5
> [class.protected]/1 thus:
>
> "Except when forming a pointer to member (5.3.1), the access must be
> through a pointer to, reference to, or object of the derived class
> itself (or any class derived from that class) (5.2.5). If the access
> is to form a pointer to member, the nested-name-specifier shall name
> the derived class (or any class derived from that class)."
>
> The first part regarding access in non-pointer-to-member case is
> perfectly clear. It's why we can't write:
>
>    struct base {
>        protected: int x
>    };
>
>    struct derived : base {
>        void foo(base* b) { b->x = 123; } // not ok
>    };
>
> The rationale is clear, too. Now the part about pointers to members
> confuses me. Applied to the above code, it means that to get pointer
> to b, we have to write &derived::x, and are forbidden from writing
> &base::x. I have always thought that this restriction is there for the
> same reason as the first one, and that the type of &derived::x would
> be a pointer-to-member-of-derived, thus preserving the strictness of
> "protected". But to my surprise, I found out recently that this isn't
> the case. Again, the Standard, 5.3.1[expr.unary.op]/2:
>
> "For a qualified-id, if the member is a static member of type    T   , the
> type of the result is plain    pointer to T.    If the member is a
> nonstatic member of class C of type T, the type of the result is
>    pointer to member of class C of type
> T.    [Example:
> struct A { int i; };
> struct B : A { };
> ... &B::i ... // has type int A::*"
>
> So this means that type of &derived::x is still (int base::*) - and
> that consequently means that I can then use that member pointer with
> any object pointer of type base* - even if it actually points at some
> other class, say, derived2, which does not inherit derived - and
> perform the very same "cross-hierarchy" protected access that the
> definition of "protected" is so carefully written to prohibit!
>

Indeed. It's been pointed out previously by AP Steinbach at
http://groups.google.com/group/comp.lang.c++.moderated/browse_thread/thread/ec22da1497adbf96
and a DR was sent in by myself at
http://groups.google.com/group/comp.std.c++/browse_thread/thread/e4d67b76e73bdac1
but to my very surprise, it never hit the core language issue list,
apparently.

>    struct base {
>        protected: int x
>    };
>
>    struct derived2 : base { };
>
>    struct derived : base {
>        void foo(base* b) {
>           b->x = 123; // not ok
>           (b->*(&derived::x)) = 123; // ok?!
>        }
>    };
>
> In fact, trying that on MSVC, g++ and Comeau, all of them agree that
> the above is perfectly fine and dandy (while &base::x does not
> compile).
>
> Is that a known problem? Is that even a problem (I find it hard to
> believe otherwise, but still...)? I didn't spot anything in active or
> closed issues or DRs, nor any changes to this in the most recent C++0x
> draft. A quick Google search didn't give any hits exploring this
> particular corner case, either.
>
It also allows modifying std::stack, std::queue and friends' encapsulated
::c member, and allows to call cout's protected non-pub functions. I would
indeed categorize this as an DR - it may be by design, but it's still a
broken design. On stackoverflow.com, it's explained too:
http://stackoverflow.com/questions/75538/hidden-features-of-
c/1065606#1065606

One could add add wording to forbid calling protected functions of standard
library components from non-priveleged contexts, to prevent such naughty
stuff from happening, i suspect :)

>
> One other thing of note is that is seems to be even more convoluted
> when virtual functions are involved. Consider this iteration with
> pointers to member functions:
>
>    struct base {
>        protected: virtual void foo() {}
>    };
>
>    struct derived2 : base { };
>
>    struct derived : base {
>        void bar(base* b) {
>           b->foo(); // not ok
>           (b->*(&derived::foo))(); // ok?!
>        }
>    };
>
> as before, this works, and for the same reason. But if I override foo
> in derived:
>
>    struct base {
>        protected: virtual void foo() {}
>    };
>
>    struct derived2 : base { };
>
>    struct derived : base {
>        virtual void foo() {}
>        void bar(base* b) {
>           b->foo(); // not ok
>           (b->*(&derived::foo))(); // not ok anymore, either!
>        }
>    };
>
> Now the type of &derived::foo is a pointer to member of derived, and
> is not compatible with type of b (and no casts are available), so the
> loophole isn't there. If the loophole is deliberate, it seems to be
> very inconsistent - since member pointer dispatch is virtual at the
> point of the call, not at the point when the pointer is obtained, why
> should virtualness of function, and presence of overrides, make any
> difference either way?
>
It's got nothing to do with virtual. It's just that name lookup found the
member in "derived" first. You can still cast "T derived::*" to "T base::*"
using static_cast, and then do The Evil Thing again.

--
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html                      ]