Ok, I've been bothered by this wicked thing for a night. Endless discussions and the ambiguity of clause 11.4(as quoted by Yam marcovic)
§ 11.4 Protected member access
[1] 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.
have burned me out. I decided to resort to the gcc source code(gcc 4.9.2 in my case) to check how those gcc guys understood the clause 11.4, and what check exactly the C++ standards wants to do and how those checks are supposed to be done.
In gcc/cp/search.c:
/* Returns nonzero if it is OK to access DECL through an object
indicated by BINFO in the context of DERIVED. */
static int protected_accessible_p (tree decl, tree derived, tree binfo)
{
access_kind access;
/* We're checking this clause from [class.access.base]
m as a member of N is protected, and the reference occurs in a
member or friend of class N, or in a member or friend of a
class P derived from N, where m as a member of P is public, private
or protected.
Here DERIVED is a possible P, DECL is m and BINFO_TYPE (binfo) is N. */
/* If DERIVED isn't derived from N, then it can't be a P. */
if (!DERIVED_FROM_P (BINFO_TYPE (binfo), derived))
return 0;
access = access_in_type (derived, decl);
/* If m is inaccessible in DERIVED, then it's not a P. */
if (access == ak_none)
return 0;
/* [class.protected]
When a friend or a member function of a derived class references
a protected nonstatic member of a base class, an access check
applies in addition to those described earlier in clause
_class.access_) Except when forming a pointer to member
(_expr.unary.op_), the access must be through a pointer to,
reference to, or object of the derived class itself (or any class
derived from that class) (_expr.ref_). 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). */
if (DECL_NONSTATIC_MEMBER_P (decl))
{
/* We can tell through what the reference is occurring by
chasing BINFO up to the root. */
tree t = binfo;
while (BINFO_INHERITANCE_CHAIN (t))
t = BINFO_INHERITANCE_CHAIN (t);
if (!DERIVED_FROM_P (derived, BINFO_TYPE (t)))
return 0;
}
return 1;
}
The most interesting part is this:
if (DECL_NONSTATIC_MEMBER_P (decl))
{
/* We can tell through what the reference is occurring by
chasing BINFO up to the root. */
tree t = binfo;
while (BINFO_INHERITANCE_CHAIN (t))
t = BINFO_INHERITANCE_CHAIN (t);
if (!DERIVED_FROM_P (derived, BINFO_TYPE (t)))
return 0;
}
1) derived in the code is the context, which in my case is the Derived class;
2) binfo in the code represents the instance whose non-static protected member is access, which in my case is base_, Derived's protected data member Base instance;
3) decl in the code represents base_.b_.
What gcc did when translating my code in question was:
1) check if base_.b_ is non-static protected member? yes of course, so enter the if;
2) climb up the inheritance tree of base_;
3) figure out what actual type base_ is; of course, it's Base
4) check if the result in 3) which is Base, derives from Derived. Of course that's a negative. Then return 0 - access denied.
Apparently, according to gcc's implementation, the "additional check" requested by the C++ standard is the type check of the instance through which the protected member gets accessed. Although the C++ standard did not explicitly mention what check should be done, I think gcc's check is the most sensible and plausible one - it's probably the kind of check the C++ standard wants. And then the question really boils down to the rationale for the standard to request an additional check like this. It effectively makes the standard contradict itself. Getting rid of that interesting section(It seems to me that the C++ standard is asking for inconsistency deliberately), the code should work perfectly. In particular, the sibling problem won't occur as it will be filtered by the statement:
if (!DERIVED_FROM_P(BINFO_TYPE(t), derived))
return 0;
Regarding the kind of protection(protected does not work purely on class, but on BOTH class AND instance) mentioned by Peter and the post(by Eric Lippert) he shared, I personally totally agree with that. Unfortunately, by looking at the C++ standard's wording, it doesn't; if we accept that the gcc implementation is an accurate interpretation of the standard, then what the C++ standard really asks for is, a protected member can be accessed by its naming class or anything that derives from the naming class; however, when the protected member is accessed via an object, make sure the owner object's type is the same as the calling context's type. Looks like the standard just wants to make an exception for the clarification point 1 in my original question.
Last but not least, I'd like to thank Yam marcovic for pointing out clause 11.4. You are the man, although your explanation wasn't quite right - the context does not have to be Base, it can be Base or anything derived from Base. The catch was in the type check of the instance through which the non-static protected member was accessed.