The key point is that protected
grants you access to your own copy of the member, not to those members in any other object. This is a common misconception, as more often than not we generalize and state protected
grants access to the member to the derived type (without explicitly stating that only to their own bases...)
Now, that is for a reason, and in general you should not access the member in a different branch of the hierarchy, as you might break the invariants on which other objects depend. Consider a type that performs an expensive calculation on some large data member (protected) and two derived types that caches the result following different strategies:
class base {
protected:
LargeData data;
// ...
public:
virtual int result() const; // expensive calculation
virtual void modify(); // modifies data
};
class cache_on_read : base {
private:
mutable bool cached;
mutable int cache_value;
// ...
virtual int result() const {
if (cached) return cache_value;
cache_value = base::result();
cached = true;
}
virtual void modify() {
cached = false;
base::modify();
}
};
class cache_on_write : base {
int result_value;
virtual int result() const {
return result_value;
}
virtual void modify() {
base::modify();
result_value = base::result();
}
};
The cache_on_read
type captures modifications to the data and marks the result as invalid, so that the next read of the value recalculates. This is a good approach if the number of writes is relatively high, as we only perform the calculation on demand (i.e. multiple modifies will not trigger recalculations). The cache_on_write
precalculates the result upfront, which might be a good strategy if the number of writes is small, and you want deterministic costs for the read (think low latency on reads).
Now, back to the original problem. Both cache strategies maintain a stricter set of invariants than the base. In the first case, the extra invariant is that cached
is true
only if data
has not been modified after the last read. In the second case, the extra invariant is that result_value
is the value of the operation at all times.
If a third derived type took a reference to a base
and accessed data
to write (if protected
allowed it to), then it would break with the invariants of the derived types.
That being said, the specification of the language is broken (personal opinion) as it leaves a backdoor to achieve that particular result. In particular, if you create a pointer to member of a member from a base in a derived type, access is checked in derived
, but the returned pointer is a pointer to member of base
, which can be applied to any base
object:
class base {
protected:
int x;
};
struct derived : base {
static void modify( base& b ) {
// b.x = 5; // error!
b.*(&derived::x) = 5; // allowed ?!?!?!
}
}