重载决议中的合并问题补充

教育   科技   2023-05-27 19:24   美国  

在第一章洞悉C++函数重载决议」中的1.2.1.1节并没有讨论继承相关的类成员名称查找规则,本文对此进行补充,下一版本(打印版)也将合并到对应章节当中。

先来看第一种情况,Multiple Inheritance

情形一:

1struct A {
2    void f() {}
3};
4
5struct B {
6    void f(int) {}
7};
8
9struct C : A, B {
10};
11
12
13int main() {
14    C c;
15    c.f();  // Error! ambiguous
16    c.f(5); // Error! ambiguous
17}

多继承时,多个基类存在相同名称,Name Lookup将会产生ambiguous。这似乎超出预料,两个函数名称并不具备相同的签名,为什么也会发生这种情况呢?
假设最终查找到的重载集为S,那么C中名称f的重载集表示为S(f, C)S主要由两部分组成,一是声明集,二是子对象集,前者是名称集,后者是名称所属的对象集。
开始查找。
首先看C中是否含有名称f,如果存在的话,将所有声明的f全都添加到声明集,将C添加到子对象集。如果不存在的话,此时就要看它是否有父类,没有则S为空,否则继续向上查找。
此时就查找到了A::fB::fAS(f, A)={ {A::f}, {A} }BS(f, B)={ {B::f}, {B} }。然后,需要将它们合并到S(f, C)={ {}, {} }。这时标准说:

if the declaration sets of S(f,Bi) and S(f,C) differ, the merge is ambiguous. (11.8.6.2)

Bi就是多个父类,说实话这句话写得让人读不懂。这里重新解释一下,在子类的声明集为空时,多个父类最终查找到的声明集也不同时(父类中的空声明集不算对比),就会存在多个不同的查找结果,合并的话就不知该保留哪一个,从而产生ambiguous。
换一种情形:
 1struct A {
2    void f() {}
3};
4
5struct B {
6    // void f(int) {}
7};
8
9struct C : A, B {
10};
11
12int main() {
13    C c;
14    c.f();  // OK
15    // c.f(5); // Error! ambiguous
16}
此时子类的声明集也为空,但多个父类查找到的声明集是唯一的,所以能够合并成功。
所以,要解决这个问题,第一种办法是不让子类的声明集为空。
 1struct A {
2    void f() {}
3};
4
5struct B {
6    void f(int) {}
7};
8
9struct C : A, B {
10    using A::f;
11    using B::f;
12};
13
14int main() {
15    C c;
16    c.f();  // OK
17    c.f(5); // OK
18}
为什么这种方式可以呢?因为标准说:

In the declaration set, using-declarations are replaced by the set of designated members that are not hidden or overridden by members of the derived class, and type declarations (including injected-class-names) are replaced by the types they designate. (11.8.3)

我们通过using-declrations为C引入了两个f重载,这绕过了合并的相关规则,所以不会产生前面的合并错误。
第二种方式是显式指定调用哪个基类里面的名称。
 1struct A {
2    void f() {}
3};
4
5struct B {
6    void f(int) {}
7};
8
9struct C : A, B {};
10
11int main() {
12    C c;
13    c.A::f();  // OK
14    c.B::f(5); // OK
15}
下面再补充一种复杂情形:
 1struct A {                      // S(f, A) = { { A::f }, { A } }
2    void f() {}
3};
4
5struct B {                      // S(f, B) = { { B:f }, { B } } 
6    void f(int) {}
7};
8
9struct C : A, B {};             // S(f, C) = { { invalid }, { A in C, B in C } }
10struct D : public virtual C {}; // S(f, D) = S(f, C)
11struct E : public virtual C {   // S(f, E) = { { E:f }, { E } } 
12    void f(char) {}
13};
14struct F : D, E {};             // S(f, F) = S(f, E)
15
16int main() {
17    F f;
18    f.f('c');  // OK
19}
FDE继承,但D中的子对象集(A,B)同时也是E中子对象集(E)的基类,那么S(f, D)被丢弃,只保留了直接基类中的E::f,这时合并不会产生ambiguous。(P.S. 经过测试gcc不支持这条标准合并规则)
最后再让我们来看不会触发合并的情形。
 1struct A {
2    void f() {}
3};
4
5struct B {
6    void f(int) {}
7};
8
9struct C : A, B {
10    void f(char) {}
11};
12
13int main() {
14    C c;
15    // c.f();  // Error, no function take 0 arguments
16    c.f(5);    // OK, call f(char)
17    c.f('c');  // OK, call f(char)
18}
因为Name Lookup在某层查找到符合名称就立即终止查找,不会再向更上一层查找,这个也叫Names shadow,所以结果不出所料,这时将不会有合并。
再来看第二种情况,Multilevel Inheritance
多重继承的情形不涉及合并规则,相对简单一点。只看一个例子:
 1struct A {
2    void f() {} // Not considered
3};
4
5struct B : A {
6    void f(int) {}
7};
8
9struct C : B {};
10
11
12int main() {
13    C c;
14    c.f(5);   // OK
15    // c.f(); // Error
16}
同样是Names shadow所以结果非常符合预期。解决方式同样是借助using-declarations让隐藏的名称显现出来。
 1struct A {
2    void f() {} // Not considered
3};
4
5struct B : A {
6    using A::f; // make every f from A available
7
8    void f(int) {}
9};
10
11struct C : B {
12};
13
14
15int main() {
16    C c;
17    c.f(5);   // OK
18    c.f();    // OK 
19}
以上是关于Class Member Lookup的补充,主要是在多继承时存在合并问题。
最后,对以上所有再做一个补充,Name Lookup发生于访问控制之前,所以即便以上名称全部处于private或protected之下,所有规则依旧适用。
有任何问题或遗漏情形,欢迎在下方留言~


CppMore
Dive deep into the C++ core, and discover more!
 最新文章