在另一个子类中访问基类的受保护成员

40

为什么这段代码可以编译通过:

class FooBase
{
protected:
    void fooBase(void);
};

class Foo : public FooBase
{
public:
    void foo(Foo& fooBar)
    {
        fooBar.fooBase();
    }
};

但这个不行吗?

class FooBase
{
protected:
    void fooBase(void);
};

class Foo : public FooBase
{
public:
    void foo(FooBase& fooBar)
    {
        fooBar.fooBase();
    }
};

一方面,C ++允许所有该类实例访问私有/受保护成员,但另一方面,它不会授予所有子类实例访问基类的受保护成员的权限。这对我来说看起来相当不一致。

我已经使用VC ++和ideone.com进行了编译测试,两者都可以编译第一个代码片段,但不能编译第二个代码片段。


@iammilind 你确定你正在关闭正确的问题吗?还是应该反过来? - Eiko
@Eiko,最初我关闭了另一个帖子并将其与此帖子合并。然后我发现那里的答案更详细,并且引用了标准。因此,我重新打开了那个帖子并关闭了这个帖子。 - iammilind
8个回答

33

foo 收到一个 FooBase 引用时,编译器无法知道参数是否为 Foo 的子孙类,因此它必须假定不是。 Foo 可以访问其他Foo对象的继承受保护成员,但不能访问所有其他兄弟类。

考虑以下代码:

class FooSibling: public FooBase { };

FooSibling sib;
Foo f;
f.foo(sib); // calls sib.fooBase()!?
如果`Foo::foo`可以调用任意`FooBase`派生类的受保护成员,那么它可以调用没有直接关系的`FooSibling`的受保护方法。这并不是受保护访问应该发挥的作用。
如果`Foo`需要访问所有`FooBase`对象的受保护成员,而不仅仅是已知为`Foo`派生类的那些对象,那么`Foo`需要成为`FooBase`的友元:
class FooBase
{
protected:
  void fooBase(void);
  friend class Foo;
};

2
啊,通过你的示例代码,为什么它不被允许的原因就很明显了。谢谢。 - Kaiserludi
我不明白你的例子。如果fooBase不是虚函数,那么你得到的是FooBase::fooBase,这正是你所指示的。如果fooBase是虚函数,实际上你调用的是FooSibling::fooBase,但这也是你使用虚函数的原因:能够根据实际对象来适应函数?我不明白这种行为何时会成为问题。 - Vincent Fourmond
虚拟在这里是无关紧要的,@Vincent。成员的虚拟状态不影响谁可以使用它的名称。成员的可见性决定了谁可以使用它的名称。Foo可以看到其他已知Foo对象的受保护成员。它不能看到任何其他对象的受保护成员,甚至不包括与其祖先类相关的那些,因为这些祖先不一定被认为是Foo。虚拟性和可见性是C++中正交的概念(但在其他语言中不一定如此)。 - Rob Kennedy
3
@RobKennedy,我理解这一点,但我无法看到任何真实的例子,需要阻止派生类从基类的另一个实例中访问受保护的成员/函数,无论它是否是虚拟的。 - Vincent Fourmond

21

C++ FAQ对这个问题进行了很好的总结:

你可以掏自己的口袋,但不能掏父亲或兄弟的口袋。

将"Original Answer"翻译成"最初的回答"。

10
你可以自由挑选你和儿子的口袋。 - Stefano Falasca

12
重点在于protected关键字允许你访问成员的副本,而不是其他任何对象中的那些成员。这是一个常见的误解,因为我们通常会概括地说protected关键字授予派生类型访问成员的权限(而没有明确指出仅限于它们自己的基类...)
现在,这是有原因的,一般来说,你不应该在层次结构的另一个分支中访问成员,因为这可能会破坏其他对象依赖的不变式。考虑一个类型,该类型对某个大数据成员(受保护)执行昂贵的计算,并且两个派生类型根据不同的策略缓存结果:
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(); 
   }
};
cache_on_read类型会记录数据的修改,并将结果标记为无效,以便下一次读取该值时进行重新计算。如果写入操作比较频繁,则这是一个不错的方法,因为我们只在需要时执行计算(即多个修改将不会触发重新计算)。cache_on_write会预先计算出结果,这可能是一个不错的策略,如果写入操作比较少,并且您想要读取具有确定成本的低延迟时间。
现在回到原始问题。这两种缓存策略都维护比基础方式更严格的不变量。在第一种情况下,额外的不变量是仅当最后一次读取后data未被修改时,cached才为true。在第二种情况下,额外的不变量是result_value始终是该操作的值。
如果第三个派生类型引用base并访问data进行写入(如果protected允许),则它将与派生类型的不变量冲突。
话虽如此,语言规范是有问题的(个人意见),因为它留下了一个后门来实现特定的结果。特别地,如果您从派生类型中的基类创建成员的成员指针,并访问它,则在derived中进行访问检查,但返回的指针是base的成员指针,这可以应用于任何base对象。
class base {
protected:
   int x;
};
struct derived : base {
   static void modify( base& b ) {
      // b.x = 5;                        // error!
      b.*(&derived::x) = 5;              // allowed ?!?!?!
   }
}

3
在这两个示例中,Foo 继承了一个受保护的方法 fooBase。然而,在第一个示例中,您尝试从相同类中访问给定的受保护方法(Foo::foo 调用 Foo::fooBase),而在第二个示例中,您尝试从另一个未被声明为友元类的类中访问受保护方法(Foo::foo 试图调用 FooBase::fooBase,但失败了,后者是受保护的)。

1
在第一个例子中,您传递了一个类型为Foo的对象,该对象显然继承了方法fooBase(),因此能够调用它。在第二个例子中,您试图调用一个受保护的函数,无论在哪个上下文中,您都不能从声明它的类实例中调用受保护的函数。
在第一个例子中,您继承了受保护的方法fooBase,因此您有权在Foo上下文中调用它。

1

我倾向于从概念和信息的角度看待事物。如果你的FooBase方法实际上被称为“SendMessage”,而Foo是“英语使用者”,FooBase是SpeakingPerson,那么你的protected声明旨在将SendMessage限制在英语使用者之间(以及子类,例如:AmericanEnglishSpeakingPerson,AustralianEnglishSpeakingPerson)。另一种派生自SpeakingPerson的FrenchSpeakingPerson类型将无法接收SendMessage,除非你将FrenchSpeakingPerson声明为friend,其中“friend”表示FrenchSpeakingPerson具有从EnglishSpeakingPerson接收SendMessage的特殊能力(即可以理解英语)。


0

除了hobo的答案,你可以寻找一个解决方法。

如果你想让子类调用fooBase方法,你可以将它设为static。静态受保护的方法可以被子类访问并带有所有参数。


0
你可以这样解决没有朋友的问题...

class FooBase
{
protected:
    void fooBase(void);
    static void fooBase(FooBase *pFooBase) { pFooBase->fooBase(); }
};

这样可以避免将派生类型添加到基类中,这似乎有点循环。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接