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

Access modifier's different behaviors in inheritance depend on "this" keyword and templates or lack thereof

本文关键字:this 关键字 取决于 继承 访问      更新时间:2023-10-16

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

这里有一个GrandChild类,privately从Parent继承,privately从具有publicenumnGrandParent继承。非对象,客户端代码可以访问GrandParent::n,因为后者是publicenum。但从GrandChild:中无法访问GrandParent::n

#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::nGrandChild中的不可访问性是由GrandChild拥有GrandParent基子对象引起的吗?该基子对象隐藏了对GrandParent::num的非对象访问,并且其两代private性使基子对象的n也不可访问?我本以为错误消息是关于这个的。

2.)但显然不是。为什么错误抱怨GrandParent的构造函数?

3.)在f()的定义中将this->预处理为GrandParent::n将添加我在#1中预期的错误,但不会删除ctor投诉为什么我假设包括this->是多余的,并且它的省略将导致查找尝试在GrandChild的作用域内的GrandParent子对象的n之前找到不太直接的作用域非对象n

4.)为什么要编译这个模板变体?它在功能上似乎与非模板的相似

#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
}

5.)在g()的定义中将this->预处理为bar<N - 2>::num只会导致我在#1中预期的编译器错误。但是为什么它不包括#2的错误呢?为什么它的遗漏没有产生#2的错误?

这里的全部问题是名称查找(我认为您之前的一个问题也是如此)。我将尝试说明我对正在发生的事情的理解:

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

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

您可以使用这个注入的类名来引用类本身。它对普通类没有那么有用(在普通类中,非限定查找无论如何都可以在周围的范围中找到名称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.uqual】/8:

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

  • 在使用它的块中或在封闭块中使用之前,或
  • 应为X类成员或X基类成员,或
  • 如果X是类Y[…]的嵌套类
  • […]
  • 如果在使用名称之前X是名称空间N的成员,或者[…],在命名空间CCD_ 53中或在CCD_

基类中的名称查找相当复杂,因为可能需要考虑多个基类。对于单个继承和在成员函数体范围内查找的成员,它从该函数所属的类开始,然后向上遍历基类(base、base of base、base的base of base等)。请参见[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的注入类名可以用作模板名(指类模板)或类型名(指当前实例化)[temp.local]/1:

与普通(非模板)类一样,类模板有一个注入的类名(第9条)。注射的-类名可以用作模板名类型名。当它与模板参数列表一起使用时,作为模板模板参数模版参数,或作为阐述类型中的最终标识符-友元类模板声明的说明符,它引用类模板本身。否则,它是等效的到模板名称,然后是<>中包含的类模板的模版参数

也就是说,bar<N - 2>找到bar作为当前类(实例化)的注入类名。当它与模板参数列表一起使用时,它引用了bar的另一个不相关的专门化。基类的注入类名是隐藏的。

bar<0>::num不是通过经过私有继承的访问路径访问,而是直接通过当前类的注入类名访问,引用类模板本身。作为CCD_ 67的公共成员的CCD_。

要想更好地解释为什么私有继承与公共和受保护继承不同,请参阅私有继承的两个好答案。

从继承的共同理解来看,C++的"私有继承"这是一个可怕的误称:它不是继承(就一切而言)类外),但是一个完整的实现类的详细信息。

从外表上看,私人继承实际上是与组成相同。只有在课堂内部,你才能比继承更令人想起的特殊语法作文

但有一点需要注意:C++在语法上将其视为继承,这带来的所有好处和问题,例如范围可见性和可访问性。此外,C样式强制转换(但没有C++演员阵容!)实际上忽略了可见性,因此成功地铸造了指向基的派生指针:

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

不用说,这是邪恶的。


公共继承意味着每个人都知道Derived是派生的从基地。

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

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

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

私人继承通常用于实现"是根据"关系来实现的。一个例子可能是Base公开了一个需要重写的虚拟函数,因此必须继承自--但您不希望客户知道具有继承关系。

以及由于私有继承而导致的不可访问类型。

这是由于从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引入作用域。