【问题标题】:C++ protected: fail to access base's protected member from within derived classC++ 保护:无法从派生类中访问基的受保护成员
【发布时间】:2016-04-07 22:25:51
【问题描述】:

诚然,这个问题的标题听起来与您邻居 Mike 反复提出的问题几乎完全相同。我发现不少问题的措辞相同,但没有一个问题与我的问题有关。

首先,我想就这个问题的上下文澄清几点:

1,c++ 访问控制是基于类而不是基于实例的。因此,下面的代码是完全有效的。

class Base
{
protected:
    int b_;

public:
    bool IsEqual(const Base& another) const
    {
        return another.b_ == b_; // access another instance's protected member
    }
};

2,我完全理解为什么以下代码无效 - 另一个可以是同级实例。

class Derived : public Base
{
public:
    // to correct the problem, change the Base& to Derived&
    bool IsEqual_Another(const Base& another) const
    {
        return another.b_ == b_;
    }
};

现在是时候提出我真正的问题了:

假设在 Derived 类中,我有一个 Base 实例数组。如此有效,Derived IS A Base(IS-A 关系),而 Derived 由 Base(复合关系)组成。我从某个地方读到这(指的是 IS-A 和 Has-A 的设计)是一种设计气味,我一开始就不应该有这样的场景。例如,分形的数学概念可以通过 IS-A 和 Has-A 关系建模。但是,让我们暂时忽略设计上的意见,只关注技术问题。

class Derived : public Base
{
protected:
    Base base_;

public:
    bool IsEqual_Another(const Derived& another) const
    {
        return another.b_ == b_;
    }

    void TestFunc()
    {
        int b = base_.b_; // fail here
    }
};

错误信息已经很清楚地说明了错误,所以你的回答中不需要重复:

Main.cpp:140:7: 错误:‘int Base::b_’受保护 诠释 b_; ^ Main.cpp:162:22:错误:在此上下文中 int b = base_.b_;

真的,根据以下两个事实,上面的代码应该可以工作:

1、C++ 访问控制是基于类而不是基于实例的(因此,请不要说我只能访问 Derived 的 b_;我不能访问独立的 Base 实例的受保护成员 - 它是基于类的) .

2,错误消息说“在此上下文中” - 上下文是派生的(我试图从派生中访问 Base 实例的受保护成员。这是受保护成员的特性 - 它应该能够从在 Base 或从 Base 派生的任何内容中。

那么为什么编译器会给我这个错误呢?

【问题讨论】:

    标签: c++ inheritance access-control protected


    【解决方案1】:

    原则上,访问规则可以为这种特殊情况提供豁免,因为已知Base最衍生的类,即对象的动态类型。但这会使事情变得复杂。 C++ 已经足够复杂了。

    一个简单的解决方法是在Base 中提供static protected 访问器函数。

    一个更骇人听闻的解决方法是使用臭名昭著的类型系统漏洞来获取成员指针。但如果我必须坚持基本设计,我会选择static 功能。因为我认为,当生成的代码一开始就很难正确编写并且维护人员难以理解时,节省一些击键没有多大意义。


    具体例子:

    class Base
    {
    protected:
        int b_;
    
        static
        auto b_of( Base& o )
            -> int&
        { return o.b; }
    
    public:
        auto IsEqual( const Base& another ) const
            -> bool
        {
            return another.b_ == b_; // access another instance's protected member
        }
    };
    

    【讨论】:

    • 我认为“静态可证明的最衍生基础”的特殊规定也是不可取的,因为即使有问题的对象确实是Base,代码outsideBase 的 /i> 不应该与受保护的成员混为一谈。 Derived 只能与它自己的 受保护的基类成员一起使用,因为它对它们具有权限。通过其他类进行的任何访问都需要 Base 的代码中 中的友元声明或其他规定,即Base 必须了解并意识到这一点。-- 这是否通常是设计指南的限制性太强当然是有争议的。
    • 赞成static 解决方法(尽管它在实践中很糟糕,但它是一个相对优雅的解决方法)。
    • 感谢您的回答。坦率地说,我认为 C++ 不应该提供这种豁免。事实上,我认为 C++ 应该禁止我最初问题中的澄清点 1。即受保护应该在类和实例的基础上工作,而不是仅仅在类的基础上。正如@PeterA.Schneider 和他的博客文章所指出的那样,我认为这种保护级别是“受保护”长期以来打算实现并且应该实现的。但我不是来制定标准的,我是来理解它的。 C++ 标准似乎自相矛盾。你怎么看?
    • @h9uest:请注意,您仍然可以无意中通过成员指针漏洞访问受保护的数据。因此,这些规则实际上有点矛盾,并不是 100% 符合某些所需的访问权限(无论所需的访问权限是什么)。所以我不认为这是通过详细设计,而只是为了简单。
    【解决方案2】:

    2,错误消息说“在此上下文中” - 上下文是派生的(我试图从派生内部访问 Base 实例的受保护成员。这是受保护成员的特性 - 它应该能够从在 Base 或从 Base 派生的任何内容中。

    好的,必须达到这个标准。

    所以你问,“为什么不可能?”答案:因为标准真正如何定义受保护的成员访问:

    § 11.4 受保护的成员访问

    [1] 当非静态数据时,应用超出第 11 条中所述的附加访问检查 成员或非静态成员函数是其命名类的受保护成员...如所述 早些时候,授予对受保护成员的访问权限是因为引用发生在某个朋友或某个 C 类的成员中

    (强调我的)

    让我们回顾一下你的例子,看看是什么。

    class Base
    {
    protected:
        int b_;
    
    public:
        bool IsEqual(const Base& another) const
        {
            return another.b_ == b_; // access another instance's protected member
        }
    };
    

    没问题。 another.b_Base::b_,我们从成员函数 Base::IsEqual(const Base&) const 访问它。

    class Derived : public Base
    {
    public:
        // to correct the problem, change the Base& to Derived&
        bool IsEqual_Another(const Base& another) const
        {
            return another.b_ == b_;
        }
    };
    

    在这里,我们再次访问Base::b_,但我们的上下文是成员函数Derived::IsEqual_Another(const Base&) const,它不是Base 的成员。所以不去了。

    现在是所谓的罪魁祸首。

    class Derived : public Base
    {
    protected:
        Base bases_[5];
    
    public:
        bool IsEqual_Another(const Derived& another) const
        {
            return another.b_ == b_;
        }
    
        void TestFunc()
        {
            int b = bases_[0].b_; // fail here
        }
    };
    

    bases_[0].b_ 正在访问受保护的Base::b_,在Derived::TestFunc() 的上下文中,它不是Base 的成员(或朋友...)。

    所以看起来编译器正在按照规则行事。

    【讨论】:

    • OP对一般情况的基本原理的解释,“另一个可以是兄弟[类]实例。”,很好。以上只是关于标准如何实现这一基本原理的。它没有回答 OP 的问题,甚至没有回答基本原理(OP 自己提供的),也没有提供任何解决方案。
    • @Cheersandhth.-Alf 怎么样?如果您正在这样做,void Derived::f() { Base b; b.b_; } 您无法访问兄弟姐妹,这仍然是一个错误。理由显然比这更大(尽管在我检查之前它让我感到惊讶)。
    • 是的,这就是 OP 的问题所在,为什么它仍然是一个错误。不是如何标准的规则使它成为错误,而是为什么
    • @Cheersandhth.-Alf 对。所以“兄弟姐妹”不是一般的理由。这里的解释显然更接近于“protected 意味着比你想象的要少”,即使他们不对我们的期望和偏见负责。这就是语言从一开始就定义它的方式。或者您是在问,“如果语言以其他方式定义它会发生什么?”
    • 嗯,兄弟姐妹是一般的理由。但是很难设计出既不复杂又完美契合的规则。所以直接访问的规则可以说太保护了,而成员数据指针的规则在某种程度上太宽松了;就这么简单。
    【解决方案3】:

    我只是将我的 cmets 变成一个答案,因为我觉得这个问题很有趣。特别是在以下最小示例中D 无法编译让我感到困惑:

    class B            { protected: int i;          };
    class D : public B { int f(B &b){ return b.i; } };
    

    毕竟,DB 并且应该能够做 B 可以做的所有事情(访问 B 的私有成员除外),不是吗?

    显然,C++ 和 C# 的语言设计者都觉得这太宽松了。 Eric Lippert commented one of his own blog posts

    但这不是我们选择的有趣或有价值的保护。 “兄弟”类不会彼此友好,因为否则保护是非常少的保护。

    编辑:
    因为对 11.4 中规定的实际规则似乎有些混淆,我将对其进行解析并用一个简短的示例说明基本思想。

    1. 列出了该部分的目的,以及它适用于什么(非静态成员)。

      除前面第 11 条所述之外的附加访问检查 在非静态数据成员或非静态成员函数时应用 是其命名类的受保护成员 (11.2)

      下例中的命名类为B

    2. 上下文是通过总结到目前为止的章节来建立的(它定义了受保护成员的访问规则)。此外,还引入了“C 类”的名称:我们的代码应该驻留在 C 的成员或友元函数中,即具有 C 的访问权限。

      如前所述,对受保护成员的访问是 授予因为 引用发生在朋友或某些成员中 C 类

      “C 类”在下面的示例中也是类C

    3. 现在才定义实际检查。第一部分处理指向成员的指针,我们在这里忽略它。第二部分涉及您日常访问对象的成员,这在逻辑上“涉及(可能是隐含的)对象表达式
      这只是描述整个部分的“附加检查”的最后一句话:

      在这种情况下,对象表达式的类 [通过它访问成员-pas] 应为 C 或从 C 派生的类。

      “对象表达式”可以是变量, 函数的返回值,或取消引用的指针。 “对象表达式的类”是编译时 属性,而不是运行时属性;通过一个访问 并且同一对象可能会被拒绝或授予,具体取决于 关于用于访问成员的表达式的类型。

    这段代码 sn-p 演示了这一点。

    class B { protected: int b; };
    
    class C: public B 
    {
        void f()
        {
            // Ok. The expression of *this is C (C has an
            // inherited member b which is accessible 
            // because it is not declared private in its
            // naming class B).
            this->b = 1;    
    
            B *pb = this;
    
            // Not ok -- the compile time 
            // type of the expression *pb is B.
            // It is not "C or a class derived from C"
            // as mandated by 11.4 in the 2011 standard.
            pb->b = 1;
        }
    };
    

    我最初想知道这条规则并假设以下理由:

    当前的问题是数据所有权和权限。

    没有代码 inside B 明确提供访问权限(通过使 C 成为朋友或类似 Alf 的静态访问器之类的东西)除了“拥有”数据的那些之外,不允许其他类访问它。这可以通过简单地定义一个兄弟并通过新的和之前的未知兄弟修改原始派生类的对象来防止非法访问类的受保护成员。 Stroustrup 在 TCPPL 的上下文中谈到了“微妙的错误”。

    虽然从派生类的代码访问原始基类的(不同的)对象是安全的,但该规则只关心表达式(编译时属性)而不是对象(运行时属性)。虽然静态代码分析可能表明某种类型的表达式 Base 实际上从不引用同级,但甚至没有尝试这样做,类似于关于别名的规则。 (也许这就是 Alf 在他的帖子中的意思。)

    我想基本的设计原则如下:保证数据的所有权和权限为类提供了保证它可以维护与数据相关的不变量(“在更改受保护的a 后,也总是更改b”)。提供通过兄弟姐妹更改受保护属性的可能性可能会破坏不变量——兄弟姐妹不知道其兄弟姐妹的实现选择的细节(这可能是在遥远的星系中编写的)。一个简单的例子是Tetragon 基类,它带有受保护的widthheight 数据成员以及普通的公共虚拟访问器。两个兄弟姐妹从它派生,ParallelogramSquareSquare 的访问器被覆盖以始终设置另一个维度,以保持正方形的等长边不变,或者它们只使用两者中的一个。现在,如果Parallelogram 可以直接通过Tertragon 引用设置Squarewidthheight,它们将破坏该不变量。

    【讨论】:

    • 彼得,您分享的博文非常棒。关于数据所有权和权限的理由,坦率地说,我完全同意你的看法。事实上,很久以前,我认为 protected 应该基于实例和类 - 即在我卸载我的问题之前的澄清点 1 不应该工作。但是,我不得不承认我之前的理解是错误的,因为:1)澄清点在实践中证明是正确的; 2)根据 C++ 标准中受保护的定义,第 237 页第 11 条,派生类可以使用受保护的名称。你怎么看?
    • @h9uest 我认为对于诸如赋值和涉及一个类型的两个实例的其他操作之类的事情,能够操作其他实例的受保护成员是合理的。
    • 恭喜彼得。 C++ 标准委员会显然也是这么想的。请参阅下面我自己的答案。我检查了 gcc 的源代码并将其与 C++ 标准进行了比较,得出的结论是标准仍然希望受保护的成员访问是基于类的,除了赋值、相等、运算符重载等场景,其中protected 成员可以通过实例直接访问。
    【解决方案4】:

    这与bases_Derived 中受到保护无关,这完全与b_Base 中受到保护有关。

    正如您已经说过的,Derived 只能访问其基类的受保护成员,而不能访问任何其他 Baseobjects。即使他们是Derived 的成员也不行。

    如果您真的需要访问权限,您可以在Base 上将Derived 加为好友。

    【讨论】:

    • 嗨,博。感谢你及时的答复。这是令人困惑的部分:在我卸载我的问题之前的澄清点 1 正是我无法说服自己的事情。你看,我正在访问一个完全独立的 Base 实例的受保护成员,编译器对此很好。
    • 作为一个测验问题我会失败的。你有这个理由吗?毕竟,Derived 是一个 Base,它似乎不会让与其他 Base 私下交互变得不合理,可以这么说,至少在受到保护的情况下是这样的 ;-)。因为 Bases 正确 可以 做到这一点。为什么从 Base 派生出一个类——毕竟,一个 Base 等等! -- 失去这种能力?
    • 为我自己的问题提供一个答案:Eric Lippert 写过关于 C# 中的等效规则,并且似乎认为 protected 的不太严格的语义不能充分保护(在他对他的评论中博客,blogs.msdn.microsoft.com/ericlippert/2005/11/09/…)。他没有详细说明,但我假设如下:给定一个哺乳动物 A,用户代码可以通过一个虚拟的兄弟哺乳动物 B“合法地”改变 A 的受保护哺乳动物状态,“攻击”A 无法“保护”——不希望设计师。
    【解决方案5】:

    好吧,我已经被这个邪恶的东西困扰了一个晚上。无休止的讨论和第 11.4 条的模棱两可(由 Yam marcovic 引用)

    § 11.4 受保护的成员访问

    [1] 当非静态数据成员或非静态成员函数是其命名类的受保护成员时,将应用第 11 条中所述之外的附加访问检查...如前所述,访问protected 成员被授予,因为引用发生在某个 C 类的朋友或成员中。

    把我烧坏了。我决定求助于 gcc 源代码(在我的例子中是 gcc 4.9.2)来检查那些 gcc 人如何理解第 11.4 条,以及 C++ 标准想要做什么检查以及应该如何进行这些检查。

    在 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;
    }
    

    最有趣的部分是:

      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) 在代码中派生的是上下文,在我的例子中是派生类;

    2)代码中的binfo代表非静态受保护成员为access的实例,在我的例子中是base_,Derived的受保护数据成员Base实例;

    3) 代码中的decl代表base_.b_。

    gcc 在翻译我的相关代码时所做的是:

    1) 检查 base__.b_ 是否为非静态受保护成员?是的,当然,所以输入 if;

    2) 爬上base_的继承树;

    3) 找出实际类型 base_ 是什么;当然是Base

    4) 检查 3) 中作为 Base 的结果是否派生自 Derived。当然,这是负面的。然后返回 0 - 拒绝访问。

    显然,根据 gcc 的实现,C++ 标准要求的“附加检查”是通过其访问受保护成员的实例的类型检查。虽然 C++ 标准没有明确提到应该做什么样的检查,但我认为 gcc 的检查是最明智和合理的——它可能是 C++ 标准想要的那种检查。然后问题真的归结为标准要求像这样进行额外检查的理由。它有效地使标准自相矛盾。摆脱那个有趣的部分(在我看来,C++ 标准是故意要求不一致的),代码应该可以完美运行。特别是不会出现兄弟问题,因为它会被语句过滤:

    if (!DERIVED_FROM_P(BINFO_TYPE(t), derived))
          return 0;
    

    关于 Peter 和他分享的帖子(由 Eric Lippert 撰写)提到的保护类型(protected 不仅仅适用于类,而是适用于类和实例),我个人完全同意这一点。不幸的是,通过查看 C++ 标准的措辞,它没有;如果我们接受 gcc 实现是对标准的准确解释,那么 C++ 标准真正要求的是,受保护的成员可以通过其命名类或从命名类派生的任何东西访问;但是,当通过对象访问受保护成员时,请确保所有者对象的类型与调用上下文的类型相同。看起来标准只是想对我原来问题中的澄清点 1 做一个例外。

    最后但同样重要的是,我要感谢 Yam marcovic 指出第 11.4 条。你就是那个人,虽然你的解释不太正确——上下文不一定是 Base,它可以是 Base 或任何从 Base 派生的东西。问题在于访问非静态受保护成员的实例的类型检查。

    【讨论】:

    • 奇怪的是,您在上面的报价忽略了实际的附加检查 11.4 授权。这会成为混乱的根源吗?您引用的内容(“授予对受保护成员的访问权限,因为...”)只是迄今为止规则的重复(“如前所述”)。实际的附加检查是“对象表达式的类 [通过其访问相关成员 -pas] 应为 C 或从 C 派生的类。” C 是源代码中位置的类。没有祖先,没有兄弟姐妹。
    • 我注意到@YamMarcovic 也没有引用重要部分。我不认为这里的标准不一致。
    • @PeterA.Schneider 是的,你是对的。感谢您指出我遗漏了实际的附加检查。我的错。然而,通过做出标准不一致的“傲慢”断言,我指的是标准想要阻止外部访问类的非静态受保护成员,而它在我的澄清点 1 中做了一个例外。原始问题。确实,这是您可以从不相关的外部上下文访问对象的非静态成员的唯一情况。
    • @PeterA.Schneider 如果您知道此例外的原因,请分享。正如您所说,“我认为对于像赋值和其他涉及类型的两个实例的其他操作,能够操作其他实例的受保护成员是合理的。”,这些用例确实很方便。但我期待一个比这更有力的理由。
    • 成员函数甚至可以访问其他实例的私有成员。否则您将如何实现副本?分配和复制等基础设施功能是必不可少的;我很难找到比这更强有力的理由。我的直觉仍然是我也将允许通过基本表达式进行访问(即允许更多)。但是兄弟问题(兄弟可能隐藏在基本表达式后面)似乎是一个很大的威慑力。我缺乏在现实生活中寻找陷阱示例的经验。
    【解决方案6】:

    有几个很长的答案,以及正确的标准引用。我打算提供一种不同的方式来看待受保护的真正含义,这可能有助于理解。

    当一个类型从不同的类型继承时,它会得到一个base 子对象。 protected 关键字意味着由于继承关系,任何派生类型都可以访问它包含的子对象中的这个特定成员。关键字授予对特定 object(s) 的访问权限,而不是对 base 类型的 任何 对象的访问。

    【讨论】: