Множественное наследование и VC++

187
26 ноября 2016, 19:07

В ходе дискуссии пришли к такой программе:

#include <iostream>
using namespace std;
class A
{
protected:
    int var;
public:
    A(int x)
    {
        var = x;    // Это обращение к A::var
    }
};
class B: public A
{
protected:
    int var;
public:
    B():A(2)
    {
        var = 4;  // Обращение к B::var
    }
};
class C: public A
{
protected:
    int var;
public:
    C():A(3)
    {
        var = 6;    // Обращение к C::var
    }
};

class D: public B, public C
{
protected:
    int var;
public:
    void method()
    {
        var = B::A::var;       // Должен выдать 2
        cout << var << endl;
        var = C::A::var;       // Должен выдать 3
        cout << var << endl;
        var = B::var;          // Должен выдать 4
        cout << var << endl;
        var = C::var;          // Должен выдать 6
        cout << var << endl;
    }
};
int main()
{
    D obj;
    obj.method();
}

Программа отлично компилируется и выводит то, что и ожидалось - в Visual C++ 2015. Попытка скомпилировать с помощью GCC на ideone.com дает массу ошибок:

prog.cpp: In member function 'void D::method()':
prog.cpp:46:21: error: 'A' is an ambiguous base of 'D'
         var = B::A::var;       // Должен выдать 2
                     ^
prog.cpp:49:21: error: 'A' is an ambiguous base of 'D'
         var = C::A::var;       // Должен выдать 3
                     ^

Вопрос к знатокам стандарта - кто тут неправ, а кто прав? Если неправ VC++, то в чем, почему и как надо поступать правильно?

Update 22.10.2016

Пожалуй, наиболее переносимо будет не полагаться на name lookup, а воспользоваться преобразованиями в духе

var = static_cast<A*>(static_cast<B*>(this))->var;
var = ((A*)(C*)this)->var;

Но при этом нужно делать var в A public'ом. (Опять же не понимаю, почему и где на это ссылка в стандарте...) Полный код тут - http://ideone.com/wt9yrz

Answer 1

Как правильно указал @Ant, студия здесь неправа и всё дело в том, что подобные обращения A::B::C::D::E::F являются именно обращениями к вложенным(nested) сущностям, т.е. B должен быть сущностью вложенной в A, С в B и так далее. Это можно видеть в не нормативной ссылке в стандарте:

[basic.lookup.qual]p2 [Note: Multiply qualified names, such as N1::N2::N3::n, can be used to refer to members of nested classes (9.7) or members of nested namespaces. — end note]

Т.е. мы можем ссылаться на несколько уровней, но эти ссылки должны идти по нисходящей, никаких восходящих, либо же смешанных уровней.

Тогда почему это вообще компилируется?(оно компилируется пока не встречает неоднозначность, уберём неоднозначность и всё заработает). Потому что есть следующее правило по поиску скрытых имён:

[class.qual]p3 A class member name hidden by a name in a nested declarative region or by the name of a derived class member can still be found if qualified by the name of its class followed by the :: operator.

И тут получается, что C::A::var должно интерпретироваться как A::var, где C:: является избыточным квалификатором, который не несёт никакой смысловой нагрузки. Никакой другой интерпретации тут быть не может, т.к. C не содержит внутреннего класса с именем A.

Суть ошибки, кстати, хорошо видна с Resharper: он сразу показывается, что квалификаторы C:: и B:: избыточны.

Что касается второй части вопроса(обновления): не работает это по простой причине: классы наследники имеют доступ к данным предка только через this, у них нет доступа к данным произвольных объектов этих классов. Это описано в [class.access.base]p5, а конкретно, случай из вопроса, в (5.3):

m as a member of N is protected, and R occurs in a member or friend of class N, or in a member or friend of a class P derived from N, where m as a member of P is public, private, or protected,

Answer 2

Прав GCC. Имена вида B::A::var и C::A::var - это не более чем квалифицированные имена, включающие в качестве nested-name-specifier имена класс-типов B::A и C::A. Это не более чем однозначный способ сослаться на сам базовый тип, в котором будет искаться имя var. И имя B::A, и имя C::A ссылаются на один и тот же базовый тип A. Он же - ::A. По этой причине оба имени эквивалентны друг другу и эквивалентны также ::A::var. То есть для расширения эксперимента вы можете добавить в функцию method() еще и доступ через ::A::var и получить абсолютно ту же саму ошибку.

Для того, чтобы подчеркнуть эту эквивалентность, можно переписать код внутри method() так

typedef B::A BA;
typedef C::A CA;
typedef ::A AA;
static_assert(std::is_same<BA, AA>::value);
static_assert(std::is_same<CA, AA>::value);
BA::var; // неоднозначность
CA::var; // неоднозначность
AA::var; // неоднозначность

Очевидно, что все три имени - BA, CA и AA - обозначают один и тот же тип. Поэтому нет никаких причин ожидать, что доступы через BA::var, CA::var или AA::var будут вести себя по-разному.

Обратите также внимание, что если вы устраните множественное наследование (и вызванную им неоднозначность), то доступ через ::A::var будет прекрасно работать внутри method(), несмотря на то, что если рассматривать его как способ задания "пути доступа", то он выглядит "неправильно".

Другими словами, nested-name-specifier в qualified-id не рассматривается языком как указание "пути прохождения" через вложенные scopes в процессе name lookup. Язык в данном случае рассматривает nested-name-specifier лишь как способ задания scope, в котором следует искать имя, после чего это имя интерпретируется "на общих основаниях" в том контексте, в котором оно использовано.

P.S. Интересно заметить, что в исходном примере MSVC допускает такое обращение

void method()
{
  var = ::A::var;
  cout << var << endl;
}

и выводит на печать 2. То есть несмотря на то, что никаких мер по устранению неоднозначности мы не предприняли, MSVC смело полагает, что "победить" в этом случае должна A::bar из B (очевидно, первой по списку базы D). Таких правил в спецификации языка нет.

READ ALSO
Ошибка с памятью в перегрузке оператора

Ошибка с памятью в перегрузке оператора

Есть вот такая перегрузка оператора ++ (постфиксная)

204
Как нарисовать в терминале линию?

Как нарисовать в терминале линию?

Мне нужно нарисовать 10 линийИз одной точки по одной линии в 10 других точек

272
Как реализовано разыменовывание указателя в C/C++?

Как реализовано разыменовывание указателя в C/C++?

Почему код int* p = &(*(&n)) корректно работает? Я, представляя себя компилятором, воспроизвожу код так:

249