Использование "использования" дважды интерпретируется разными разными компиляторами

Рассмотрим следующий код:

struct A {
    int propose();
};

struct A1 : A {
    int propose(int);
    using A::propose;
};

struct B1 : A1 {
protected:
    using A1::propose;
public:
    using A::propose;
};

int main() {
    B1().propose();
}

Скомпилируйте это: g++ -std=c++11 main.cpp.

Я получаю следующую ошибку компилятора с помощью GNU 4.8.1:

main.cpp: In function 'int main()':                                                                                                                    
main.cpp:2:9: error: 'int A::propose()' is inaccessible
     int propose();
         ^
main.cpp:18:18: error: within this context
     B1().propose();

Однако этот код компилируется в AppleClang 6.0.0.6000056.

Я понимаю, что нет необходимости в using в B1 (в моем коде был необходим, но по ошибке я имел 1 using). В любом случае, почему Кланг компилирует его? Ожидается ли это?

Ответы

Ответ 1

В [namespace.udecl] мы имеем:

Когда декларация использования приносит имена из базового класса в область производного класса, функции-члены и шаблоны функций-членов в переопределении производного класса и/или скрыть функции-члены и функцию-член шаблоны с тем же именем, список параметров (8.3.5), cv-qualification и ref-qualifier (если есть) в базовый класс (а не конфликтующий).

В стандарте явно указано, что введенные имена не будут конфликтовать с именами в базовом классе. Но он ничего не говорит о конфликтующих именах.

В разделе также говорится:

Использование-объявления является декларацией и поэтому может использоваться повторно, где (и только там) несколько объявления разрешены. [Пример:

struct B {
    int i;
};

struct X : B {
    using B::i;
    using B::i; // error: double member declaration
};

-end пример]

И интересно, в следующем примере это GCC, который с радостью компилирует его (и печатает A), в то время как Clang позволяет построить C, но отклоняет вызов foo как двусмысленный:

struct A {
    void foo() { std::cout << "A\n"; }
};

struct B {
    void foo() { std::cout << "B\n"; }
};

struct C : A, B {
    using A::foo;
    using B::foo;
};


int main()
{
    C{}.foo();
    return 0;
}

Итак, короткий ответ - я подозреваю, что это недоказано в стандарте и что оба компилятора делают приемлемые вещи. Я бы просто отказался от написания такого кода для общего здравомыслия.

Ответ 2

Декларация является законной.

Вызов, который является законным и должен работать в любом месте, и его можно вызывать только из класса и производных классов, и он может быть вызван из любого класса. Вы заметите, что это мало смысла.

Нет правил, запрещающих эту конструкцию в объявлениях (импортируя имя дважды из двух разных базовых классов с одной и той же сигнатурой), и даже используется в "реальном" коде, где производный класс отправляется и скрывает имя после того, как они импортируются.

Если вы не скрываете это, вы находитесь в странной ситуации, когда одна и та же функция A::propose является и защищенной и общедоступной одновременно, так как она дважды называется (юридически) в та же область с различным контролем доступа. Это... необычно.

Если вы находитесь в классе, в подзапросе говорится, что вы можете его использовать:

[class.access.base]/5.1

Член m доступен в точке R, когда он назван в классе N, если - (5.1) m как член из N является общедоступным

и propose явно публично. (это также protected, но нам не нужно читать в этом случае!)

В другом месте мы имеем противоречие. Вам говорят, что вы можете использовать его везде без ограничений [class.access]/1 (3). И вам говорят, что вы можете использовать его только в определенных обстоятельствах [class.access]/1 (2).

Я не уверен, как разрешить эту двусмысленность.


Остальная часть логического цикла:

В [namespace.udecl]/10 мы имеем:

Использование-декларация является объявлением и поэтому может использоваться повторно там, где (и только там) разрешено несколько деклараций.

И [namespace.udecl]/13:

Так как декларация использования является объявлением, ограничения на объявления с тем же именем в той же декларативной области

поэтому каждый из этих using X::propose; является декларациями.

[basic.scope] не имеет применимых ограничений для двух функций с одним и тем же именем в области, отличных от [basic.scope.class]/1 (3), который гласит, что если переупорядочение объявлений изменяет программу, программа плохо сформирован. Поэтому мы не можем сказать, что побеждает более поздний.

Две декларации функций-членов в той же области действия являются законными в соответствии с [basic.scope]. Однако в разделе [over] существуют ограничения на две функции-члены с тем же именем.

[over]/1 состояния:

Если для одного имени в одной и той же области указано два или более разных объявления, это имя считается перегруженным

И есть некоторые ограничения на перегрузку. Это то, что обычно предотвращает

struct foo {
  int method();
  int method();
};

от юридического. Однако:

[over.load]/1 состояния:

Не все объявления функций могут быть перегружены. Здесь указаны те, которые не могут быть перегружены. Программа плохо сформирована, если она содержит два таких неперегружаемых объявления в одной области. [Примечание: это ограничение применяется к явным объявлениям в области видимости и между такими декларациями и декларациями, сделанными с помощью декларации использования (7.3.3). Он не применяется к наборам функций, сгенерированных в результате поиска имени (например, из-за использования директив) или разрешения перегрузки (например, для функций оператора). -end note

в заметке явно разрешены символы, введенные через два использования-декларации, из-за ограничений перегрузки! Правила применяются только к двум явным объявлениям в пределах области видимости или между явным объявлением внутри области действия и декларацией использования.

На двух объявлениях-объявлениях есть нулевые ограничения. Они могут иметь одно и то же имя, и их подписи могут конфликтовать столько, сколько захотите.

Это полезно, потому что обычно вы можете пойти, а затем скрыть свое объявление (с объявлением в производном классе), и ничего не получается [namespace.udecl]/15:

Когда декларация использования приносит имена из базового класса в область производного класса, функции-члены и шаблоны функций-членов в производном классе переопределяют и/или скрывают функции-члены и шаблоны-члены-члены с тем же именем, список (8.3.5), cv-qualification и ref-qualifier (если есть) в базовом классе (а не в конфликте).

Однако это не делается здесь. Затем мы вызываем метод. Выдается разрешение перегрузки.

См. [namespace.udecl]/16:

В целях разрешения перегрузки функции, вводимые использование-объявления в производный класс будет рассматриваться так, как если бы они были членами производного класса. В частности, неявный этот параметр обрабатывается так, как если бы он был указателем на производный класс, а не на базовый класс. Это не влияет на тип функции, и во всех остальных отношениях функция остается членом базовый класс.

Поэтому мы должны рассматривать их так, как если бы они были членами производного класса с целью разрешения перегрузки. Но здесь есть еще 3 объявления:

protected:
  int A::propose(); // X
  int A1::propose(int); // Y
public:
  int A::propose(); // Z

Таким образом, вызов B1().propose() учитывает все 3 объявления. Оба X и Z равны. Однако они относятся к одной и той же функции, а состояния разрешения перегрузки - двусмысленность, если выбраны две различные функции. Поэтому результат не является двусмысленным. Там могут быть нарушения контроля доступа или нет, в зависимости от того, как вы читаете стандарт.

[over.match]/3

Если наилучшая жизнеспособная функция существует и уникальна, разрешение перегрузки преуспевает и дает результат в результате. В противном случае разрешение перегрузки выходит из строя, а вызов плохо сформирован. Когда разрешение перегрузки преуспевает, и наилучшая жизнеспособная функция недоступна (раздел 11) в том контексте, в котором она используется, программа плохо сформирована.