菱形层次结构中的虚函数重载在 clang 和 gcc 中产生不同的结果



以下代码在 clang 上产生不同的结果。

#include <iostream>
struct Dummy1 {};
struct Dummy2 {};
struct A {
virtual void foo(Dummy1) {
std::cout << "A" << std::endl;
}
virtual void foo(Dummy2) {
std::cout << "A" << std::endl;
}
};

template<class T>
struct C : virtual A {
using A::foo;
void foo(Dummy2) override {
std::cout << "C" << std::endl;
}   
};

template<class T>
struct B : virtual A {
using A::foo;
void foo(Dummy1) final {
std::cout << "B" << std::endl;
}   
};
template<class T>
struct D : B<T>, C<T> {
// using B<T>::foo; // error: call to member function 'foo' is ambiguous
// using C<T>::foo; // error: call to member function 'foo' is ambiguous
using A::foo;
};

int main() {
D<int> d;
d.foo(Dummy1{});
d.foo(Dummy2{});
A& a = d;
a.foo(Dummy1{});
a.foo(Dummy2{});
B<int>& b = d;
b.foo(Dummy1{});
b.foo(Dummy2{});

C<int>& c =d;
c.foo(Dummy1{});
c.foo(Dummy2{});
return 0;
}

gcc (版本 4.8.1 - 9.1), ICC (版本 16, 17, 19), Visual Studio 2017 15.4.0 预览版 1.0,Visual Studio 2013 12.0.31101.00 Update 4, clang (版本 3.4.1 - 3.9.1)

所有这些都给出了以下输出,这是我所期望的:

B
C
B
C
B
C
B
C

仅选择方法C<T>::foo(Dummy1)B<T>::foo(Dummy2),不使用方法A<T>::foo

Clang (版本 4.0.0 - 8.0.0) 在通过对象调用时选择A::foo(Dummy2)D<T>并且仅在该对象时选择。当通过引用B<T>-C<T>::foo(Dummy2)调用时,将选择此选项。

B
A <-- difference
B
C
B
C
B
C

当派生类的顺序更改为struct D : C<T>, B<T>时,输出将更改为:

A <--
C
B
C
B
C
B
C

似乎对于第二个派生类方法,foo不被认为是虚拟的。

只有Visual Studio发出警告,没有那么有用的C4250。

D<T>而不是using A::foo;编写using B<T>::foo;using C<T>::foo;会使 clang 产生以下错误:

错误:对成员函数"FOO"的调用不明确

在 gcc 行为不变时,代码编译和输出是相同的。

什么是正确的行为?

由于应用程序给出不同的结果,有没有办法找到此构造的所有类似实例或进行一些解决方法?我必须同时使用 gcc 和 clang 进行编译。检查是否在比我发现的更多的地方存在相同的问题可能很困难。

我相信这里有两件事发生。

首先,clang 的实现肯定存在问题,因此提交错误报告是一个很好的举措。

但是当您在D引用上调用 foo 时也存在一个问题

D<int> d;
d.foo(Dummy1{});
d.foo(Dummy2{});

Visual Studio 给你的警告实际上非常准确:从技术上讲,每个调用中有两个选项,但一个是通过支配权选择

的。对于Dummy1重载,类B中有被覆盖的实现,但也有类C中的未覆盖实现。这就是警告所指的隐藏另一个警告的"主导"警告。 在这种情况下,主要版本是类B中的被覆盖版本,而弱版本是类C中的版本。如果你在类C中也覆盖了Dummy1重载,那么你就会有一个模棱两可的调用。

对于Dummy2过载也是如此。

因此,编译器警告您的是,在已知D实例的情况下,您实际上有一个选择,并且应该明确说明它。

最新更新