Неоднозначность при множественном наследовании

Отвечал на вопрос об одинаковых именах при наследовании. Написал такой код (см. ниже). Visual C++ его компилирует и выполняет на ура. Но масса прочих компиляторов ругается на

d.C::A::x = 2;
d.B::A::x = 3;

и вызовы out(), утверждая, что тут неоднозначность в выборе A::x. Откровенно говоря, я ее не вижу; вывод у VC++ соответствует ожидаемому.

Кто-то может доказать правоту того или иного компилятора явной ссылкой на пункт стандарта? Откуда тут неоднозначность — при явном указании пути через B или C? Как ее устранить в таких компиляторах, кто недоволен?

#include <iostream>
using namespace std;
struct A
{
    int x;
    void out() { cout << x << endl; }
};
struct B: public A
{
    int x;
    void out() { cout << x << endl; }
};
struct C: public A
{
    int x;
    void out() { cout << x << endl; }
};
struct D: public B, public C
{
    int x;
    void out() { cout << x << endl; }
};

int main()
{
    D d;
    d.C::x = 0;
    d.B::x = 1;
    d.C::A::x = 2;  // неоднозначность в не-MSVC
    d.B::A::x = 3;  // неоднозначность в не-MSVC

    d.B::A::out();  // неоднозначность в не-MSVC
    d.C::A::out();  // неоднозначность в не-MSVC
    d.B::out();
    d.C::out();
}

Ответы (1 шт):

Автор решения: HolyBlackCat

Когда вы пишете B::A (причем неважно, что это после . - работает везде одинаково) - это то же самое, что просто A (если B - наследник A), поэтому d.C::A::x превращается в d.A::x - а это явно неоднозначность и ошибка.

  • Почему B::A - то же самое, что A?

    Из-за injected-class-name. Injected-class-name - это что-то вроде скрытого using ИмяКласса = ИмяКласса;, который создается автоматически в каждом классе, и наследуется точно так же, как и все остальные члены класса.

    То есть B наследует из A что-то вроде using A = A;, поэтому B::A означает A.

    (Из этого также должно следовать, что A::A означает A, но вот это уже работает не везде: struct B : A::A {}; работает, а A::A a; уже не работает - из-за [class.qual]/1 (во втором случае компилятор считает A::A именем конструктора A - как при наследовании конструктора - using A::A;).)

  • Как это :: справа от . работает по тем же правилам, что и в любом другом месте? Ведь казалось бы, A::x (где x - нестатический член) работает только после .!

    А вот и нет, A::x для нестатических членов работает много где. Везде, где результат выражения не вычисляется, например в sizeof и decltype:

    #include <type_traits>
    
    struct A {int x;};
    
    static_assert(sizeof(A::x) == sizeof(int));
    static_assert(std::is_same_v<decltype(A::x), int>);
    

    А как вам такое:

    static_assert(sizeof(A::x + 42) == sizeof(int));
    static_assert(std::is_same_v<decltype(A::x + 42), int>);
    

В целом, в стандарте нигде не описывается поведение :: конкретно справа от . Он работает везде одинаково. То есть нет никакого процесса "последовательного захода в родителей родителей"; единственная причина, почему у вас получилось указать нескольких родителей после в d.C::A::x - это injected-class-name.

Явно про ваш случай говорит [expr.ref]/7:

[в выражении вида E1.E2...]

If E2 is a non-static member, the program is ill-formed if the class of which E2 is directly a member is an ambiguous base ([class.member.lookup]) of the naming class ([class.access.base]) of E2.

Здесь "naming class" ("именующий/называющий/обращающийся класс"?) - это тип объекта слева от точки.


Есть другой способ обратиться к полю родителя - кастануть объект в ссылку на родителя:

static_cast<A &>(static_cast<C &>(d)).x

Или так:

static_cast<C &>(d).A::x
→ Ссылка