访问修饰符在继承中的不同行为取决于“this”关键字和模板或缺少

时间:2013-12-27 23:00:42

标签: c++ templates inheritance this access-modifiers

关于使用和/或省略template s和this关键字的4种组合,我想了解访问修饰符有关继承的4种不同行为。以下所有代码均在g ++ 4.8中完成:

这是一个GrandChild类,private继承自Parentprivate继承自GrandParent,其public enum n。非对象客户端代码可以访问GrandParent::n,因为后者是public enum。但GrandParent::n内无法访问GrandChild

#include <iostream>
using namespace std;

struct GrandParent { enum {n = 0}; };

struct Parent : private GrandParent { enum {n = 1}; };

struct GrandChild : private Parent {
    enum {n = 2};
    void f() {cout << GrandParent::n << endl;}
    // ^ error: 'struct GrandParent GrandParent::GrandParent'
    // is inaccessible
};
int main() {
    cout << GrandParent::n << endl;
    // ^ non-object access would have outputted `0` had `GrandChild`'s
    // definition compiled or been commented out.
}

1。)由GrandParent::n拥有GrandChild基础子对象导致GrandChild GrandParent内部GrandParent::num无法访问,该子对象隐藏了对象的非对象访问权限{ {1}},其2代private使得基础子对象的n也无法访问?我希望错误信息是关于那个。

2。)但显然,事实并非如此。为什么错误会抱怨GrandParent的构造函数?

3。)在this->的定义中预先GrandParent::nf()会添加我在#1中预期的错误,但不会删除ctor投诉。 为什么?我认为包含this->是多余的,而且遗漏会导致查找尝试在{{1}内找到n子对象的GrandParent无论如何,在范围较小的非对象GrandChild之前的范围。

4.。)为什么这个模板变体会编译?它看起来在功能上与非模板类似:

n

5.在#include <iostream> using namespace std; template <unsigned int N> struct bar : private bar<N - 1> { enum {num = N}; void g() { static_assert(N >= 2, "range error"); cout << bar<N - 2>::num << endl; } }; template <> struct bar<0> { enum {num = 0}; }; int main() { bar<2> b2; b2.g(); // Output: 0 } 的定义中将this->前置到bar<N - 2>::num会导致我在#1中预期的编译器错误。但为什么不包括#2的错误?为什么它的遗漏没有产生#2的错误?

2 个答案:

答案 0 :(得分:3)

这里的整个问题是名称查找(我认为情况也是如此in one of your previous questions)。我会试着说明我对正在发生的事情的理解:

每个(命名)类都会获得一个注入类名。例如:

struct GrandParent
{
    // using GrandParent = ::GrandParent;
    enum {n = 0};
};

您可以使用此inject-class-name来引用类本身。它对普通类没有用(其中非限定查找无论如何都可以在周围的范围中找到名称GrandParent),但是对于派生类和类模板:

namespace A
{
    struct Foo
    {
        // using Foo = ::A::Foo;
    };
};

struct Bar : A::Foo
{
    void woof(Foo); // using the injected-class-name `::A::Foo::Foo`
};

template<class T, int N, bool b>
struct my_template
{
    // using my_template = ::my_template<T, N, b>;
    void meow(my_template); // using the injected-class-name
};

这不是“它是基类子对象的一部分”中的继承,但是指定了非限定查找的方式:如果在当前类的范围中找不到该名称,则将搜索基类范围。

现在,对于OP中的第一个(非模板)示例:

struct Parent : private GrandParent
{
    // using Parent = ::Parent;

    enum {n = 1}; // hides GrandParent::n
};

struct GrandChild : private Parent {
    // using GrandChild = ::GrandChild;

    enum {n = 2};
    void f() {cout << GrandParent::n << endl;}
    // ^ error: 'struct GrandParent GrandParent::GrandParent'
    // is inaccessible
};

此处,表达式GrandParent::n调用名称GrandParent的非限定名称查找。由于未找到限定的查找在找到名称时停止(并且不考虑周围的范围),因此它将找到注入的类名GrandParent::GrandParent。也就是说,查找搜索GrandChild(未找到名称)的范围,然后搜索Parent(未找到名称)的范围,最后搜索GrandParent的范围(它找到注入的范围)班级名称)。这是在之前独立于访问检查完成的。

找到名称GrandParent后,最终会检查辅助功能。需要进行名称查找才能从Parent转到GrandParent以查找名称。除Parent的成员和朋友之外的任何人都阻止此路径,因为继承是私有的。 (您可以看到该路径,但您可能无法使用它;可见性和可访问性是正交概念。)


这是标准[basic.lookup.unqual] / 8:

  

对于类X的成员,成员函数体[...]中使用的名称应以下列方式之一声明:

     
      
  • 在用于使用它的块中或在封闭块中使用之前,或
  •   
  • 应为班级X的成员,或者是X基类的成员,或
  •   
  • 如果X是类Y [...]
  • 的嵌套类   
  • [...]
  •   
  • 如果X是名称空间N的成员,或[...],则在使用名称之前,   在名称空间NN的一个名称空间中。
  •   

基类中的名称查找相当复杂,因为可能必须考虑多个基类。对于单继承和在成员函数体范围内查找的成员,它从该函数所属的类开始,然后遍历基类(基数,基数,基数的基数, ..)。见[class.member.lookup]


模板大小写不同,因为bar是类模板的名称:

template <unsigned int N>
struct bar : private bar<N - 1> {
    enum {num = N};
    void g() {
        static_assert(N >= 2, "range error");
        cout << bar<N - 2>::num << endl;
    }
};

此处使用bar<N - 2>。它是一个从属名称,因为N是一个模板参数。因此,名称查找被推迟到g的实例化点。可以找到专门化bar<0>,即使它是在函数之后声明的。

bar的inject-name-name可以用作模板名称(指类模板)或类型名称(指当前实例化)[temp.local] / 1 :

  

与普通(非模板)类一样,类模板具有注入类名(第9节)。注入 -   class-name可用作模板名称类型名称。当它与 template-argument-list 一起使用时,   作为模板模板参数模板参数,或作为详细说明类型中的最终标识符 -   友元类模板声明的说明符,它指的是类模板本身。否则,它是等价的   到 template-name 后跟<>中包含的类模板的 template-parameters

也就是说,bar<N - 2>找到bar作为当前类(实例化)的注入类名。由于它与 template-argument-list 一起使用,因此它引用了bar的另一个不相关的特化。隐藏了基类的inject-class-name。

bar<0>::num通过访问路径访问而不是,该访问路径通过私有继承,但直接通过当前类的inject-class-name引用类模板本身。 numbar<0>的公共成员,可以访问。

答案 1 :(得分:1)

为了更好地解释为什么私有继承不像公共继承和受保护的继承,请参阅private inheritance中的两个好答案。

  

从对继承的共同理解,C ++'“私有继承”   这是一个可怕的用词:它不是继承(就所有事情而言)   在课外有关)但是完整的实施   班上的细节。

     

从外部看,私人继承实际上就是这样   与作文相同。只有在班级内部才能得到   比继承更容易让人想起继承的特殊语法   组合物。

     

但有一点需要注意:C ++在语法上将其视为继承,   具有所需的所有好处和问题,例如范围   可见性和可访问性。此外,C风格的演员阵容(但没有C ++   演员!)实际上忽略了可见性,从而成功地投射你的   派生指向Base的指针:

     

Base* bPtr = (Base*) new Derived();

     

毋庸置疑,这是邪恶的。

     
     

公共继承意味着每个人都知道Derived是派生的   来自Base。

     

受保护的继承意味着只有Derived,Derived的朋友和   派生自Derived的类知道Derived派生自Base。*

     

私有继承意味着只有Derived和Derived的朋友   知道Derived是从Base。派生的。

     

由于您使用了私有继承,因此main()函数没有   关于从base派生的线索,因此无法分配指针。

     

私有继承通常用于实现   “与......有关的实施”。一个例子可能是   Base公开了一个你需要覆盖的虚函数 - 因此   必须继承 - 但你不希望客户知道你   有继承关系。

以及Inaccessible type due to private inheritance

  

这是由于A中注入的类名隐藏了全局A.   在C.内部虽然A是可见的,但它是无法访问的(因为它是   导入为私有),因此错误。您可以通过查看来访问A.   在全局命名空间中:

     

void foo(::A const& a) {}

例如,这将起作用:

class GrandChild;
struct GrandParent { enum {n = 0}; };

struct Parent : private GrandParent { 
   enum {n = 1}; 
   friend GrandChild; 
};

struct GrandChild : private Parent {
    void f() {cout << GrandParent::n << endl;}
};

否则,您需要使用全局范围或using指令将::GrandParent纳入范围。