钻石继承和作用域解析运算符(C++)

13

我有这段代码(钻石问题):

#include <iostream>
using namespace std;

struct Top
{
    void print() { cout << "Top::print()" << endl; }
};

struct Right : Top 
{
    void print() { cout << "Right::print()" << endl; }
};

struct Left : Top 
{
    void print() { cout << "Left::print()" << endl; }
};

struct Bottom: Right, Left{};

int main()
{
    Bottom b;
    b.Right::Top::print();
}

我想在Top类中调用print()

当我尝试编译时,我得到了错误:'Top'是'Bottom'的模棱两可的基类,出现在这一行:b.Right::Top::print(); 为什么会有歧义?我明确指定了我想要来自Right而不是LeftTop

我不想知道如何完成它,是的,可以使用引用、虚继承等方法。我只想知道为什么b.Right::Top::print();存在歧义。


这段代码含糊不清,根据11.2p6规定,如果使用类成员访问运算符(包括隐式的“this->”)来访问非静态数据成员或非静态成员函数,则如果左操作数(在“.”运算符情况下被视为指针)不能隐式转换为右操作数所属的命名类的指针,则引用是不合法的。请注意,命名类是A,但D*无法隐式转换为A* - Johannes Schaub - litb
这里的语义是,你使用 B::A::tell 告诉它你想调用哪个函数。你通过使用 D::tell 来帮助编译器,即名称查找。但你不指定它必须使用的子对象 - 它将有两个选择:沿着 B 上的 A 或沿着 C 上的路走,并给出错误提示。 - Johannes Schaub - litb
在运行时操作对象的上下文中,有两个“主要”的歧义检查:一个在5.2.5p5中进行,另一个在11.2p6中咬你。5.2.5p5中的一个拒绝d.tell(),如果您删除除A的tell函数之外的所有tell函数,则命名类将为D,但是tell将是A的直接成员,而A是模糊的。如果您然后说D.B::A::tell(),则根据5.2.5p5,它是良好形式的,但根据11.2p6,它是不良形式的。这些检查相互补充,在类型系统中正常运行非常重要。 - Johannes Schaub - litb
这里有一个相关的问题,MSVC++也接受:https://dev59.com/AlHTa4cB1Zd3GeqPULwa - Johannes Schaub - litb
3个回答

16
为什么它是含糊的?我明确指定我想从“右侧”获取Top,而不是从“左侧”获取。

那确实是你的意图,但实际上并不是这样发生的。Right::Top::print() 明确命名了要调用的成员函数,即 &Top::print。但它没有指定在哪个子对象b 上调用该成员函数。从概念上讲,你的代码等效于:
auto print = &Bottom::Right::Top::print;  // ok
(b.*print)();                             // error

选择 print 的部分是明确的。不明确的是从 bTop 的隐式转换。您需要明确消除歧义,指明您要执行的方向,例如:

static_cast<Right&>(b).Top::print();

如果我使用Right::print()而不是Right::Top::print(),为什么它能工作呢?两者都无法与指向成员的指针一起使用,但是b.Right::print()可以工作。(当然,我已经在Right类中删除了print())。 - PcAF
1
@PcAF 因为从 bRight 的转换是明确的。只有一个类型为 Right 的子对象。 - Barry
感谢您的回复。如果从 bRight 的转换是明确的,那么为什么这样:auto print = &Bottom::Right::print; (b.*print)() 会导致 ambiguous base 错误(Right 是空类)? - PcAF
@PcAF 这并不含糊。尤其是你发布的层级关系清晰易懂。 - Barry
在两个注释中,我写道print()已被删除。 - PcAF
1
关于您的后续评论@PcAF。使用&Bottom::Right::print是不明确的,因为它实际上的类型是void(Top::*)()。您使用Bottom::Right而不是Bottom::Right::Top的信息并没有被类型系统记住。 - Johannes Schaub - litb

4
作用域解析运算符是左结合的(尽管它不允许括号)。因此,当您想在B中引用A::tell时,id表达式是指B::A内的tell,即简单的A,这是含糊的。解决方法是首先转换为明确的基类B,然后再转换为A。语言法律:[basic.lookup.qual]/1说,类或命名空间成员或枚举器的名称可以在表示其类、命名空间或枚举器的嵌套名称限定符之后的::作用域解析运算符之后被引用。嵌套名称限定符的相关语法是:嵌套名称限定符:typename :: nested-name-specifier identifier ::。因此,第一个嵌套名称限定符是B::,并且在其中查找A。然后,B::A是一个嵌套名称限定符,表示A,并且在其中查找tell。显然,MSVC接受该示例。可能它具有非标准扩展,通过回溯这样的限定符来解决歧义。

请引用相关的标准文档,否则这就是猜测。 - Cheers and hth. - Alf
@Cheersandhth.-Alf 我发布后添加了 [language-lawyer] 标签。我会尽力提供一个好的解释,但请注意,从右到左进行查找是不可能的。 - Potatoswatter
@Potatoswatter +1,请参考我的评论以获取更多信息。 - Johannes Schaub - litb
我鼓励你将你的答案移动到这里:https://dev59.com/VVoV5IYBdhLWcg3wCrEn,而不是在这个重复的问题上。 - Johannes Schaub - litb

1

实际上,给出的代码在 Visual Studio 2019 上运行良好。 解决菱形继承问题有两种方法: - 使用作用域解析运算符 - 将基类声明为虚拟继承

通过 b.Right::Top::print() 调用 print 函数应该可以无错误执行。但是你的 Bottom 类仍然引用了两个 Top 类的对象。

你可以在 这里 找到更多详细信息。


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