Подтвердить что ты не робот

Оператор [] поиск в базовых классах шаблонов

Следующий код вызывает у нас небольшую головную боль: clang и MSVC принимают следующий код, а GCC отвергает его. Мы полагаем, что GCC прав на этот раз, но я хотел убедиться, что перед подачей отчетов об ошибках. Итак, существуют ли какие-либо специальные правила для поиска operator[], о которых я не знаю?

struct X{};
struct Y{};

template<typename T>
struct B
{
    void f(X) { }
    void operator[](X){}
};

template<typename T>
struct C
{
    void f(Y) { }
    void operator[](Y){}
};

template<typename T> struct D : B<T>, C<T> {};

int main()
{
    D<float> d;
    //d.f(X()); //This is erroneous in all compilers
    d[Y()];//this is accepted by clang and MSVC
}

Итак, приведенный выше код является правильным при разрешении вызова operator[] в функции main?

4b9b3361

Ответ 1

Я считаю, что Clang и MSVC неверны, и GCC правильно отклоняет этот код. Это пример того, что имена в разных областях не перегружаются друг с другом. Я представил это Clang как llvm bug 26850, мы увидим, согласны ли они.

Нет ничего особенного в operator[] vs f(). Из [over.sub]:

operator[] должна быть нестатической функцией-членом только с одним параметром. [...] Таким образом, подстрочное выражение x[y] интерпретируется как x.operator[](y) для объекта класса x типа Tесли T::operator[](T1) существует и если оператор выбран как наилучшая функция соответствия при перегрузке механизм разрешения

Таким образом, правила, определяющие поиск d[Y()], совпадают с правилами, определяющими d.f(X()). Все компиляторы были верны, чтобы отвергнуть последнее, и должны были также отвергнуть первое. Более того, как Clang, так и MSVC отклоняют

d.operator[](Y());

где оба они принимают:

d[Y()];

несмотря на то, что они имеют одинаковое значение. Нет не-член operator[], и это не вызов функции, поэтому также не существует зависимого от аргумента поиска.

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


Правила поиска имени члена определены в [class.member.lookup]. Это уже немного сложно разобрать, плюс он относится к C как к объекту, который мы просматриваем (который в OP называется D, тогда как C является подобъектом). У нас есть это понятие поиска:

Набор поиска для f в C , называемый S(f,C), состоит из двух компонентов: набора объявлений, набора члены с именем f; и совокупность подобъектов, множество подобъектов, где объявления этих членов (возможно включая использование-деклараций). В наборе объявлений использование-объявления заменяются набором назначенных членов, которые не скрыты или переопределены членами производного класса (7.3.3), и типа декларации (включая имена введенных классов) заменяются типами, которые они обозначают.

Объявление, установленное для operator[] в D<float>, пусто: не существует явного объявления или декларации использования.

В противном случае (т.е. C не содержит декларации f или результирующий набор объявлений пуст), S(f,C) is изначально пуст. Если C имеет базовые классы, вычислите набор поиска для f в каждом подобъекте прямого подкласса B i, и объединить каждый такой набор поиска S (f, B i) в свою очередь в S(f,C).

Итак, мы рассмотрим B<float> и C<float>.

Следующие шаги определяют результат слияния поискового набора S (f, B i) в промежуточное звено S (f, C): - Если каждый из подобъектов S (f, B i) является подобъектом базового класса, по меньшей мере, одного из подобъектов члены S (f, C), или если S (f, B i) пуст, S (f, C) не изменяется и слияние завершено. Наоборот, если каждый из субобъектов S (f, C) является подобъектом базового класса, по меньшей мере, одного из субобъекты S (f, B i), или если S (f, C) пуст, новый S (f, C) является копией S (f, B iсуб > ).
- В противном случае, если наборы объявлений S (f, B i) и S (f, C) отличаются, слияние неоднозначно: новый S (f, C) - это набор поиска с недопустимым набором объявлений и объединением наборов подобъектов. В последующих сливается, недопустимый набор объявлений считается отличным от любого другого.
- В противном случае новый S (f, C) представляет собой набор поиска с общим набором объявлений и объединением подобъектов. Результатом поиска имени для f в C является набор объявлений S(f,C). Если это недопустимый набор, программа плохо сформирован. [Пример:

struct A { int x; }; // S(x,A) = { { A::x }, { A } }
struct B { float x; }; // S(x,B) = { { B::x }, { B } }
struct C: public A, public B { }; // S(x,C) = { invalid, { A in C, B in C } }
struct D: public virtual C { }; // S(x,D) = S(x,C)
struct E: public virtual C { char x; }; // S(x,E) = { { E::x }, { E } }
struct F: public D, public E { }; // S(x,F) = S(x,E)
int main() {
    F f;
    f.x = 0; // OK, lookup finds E::x
}

S(x, F) однозначен, поскольку базовые подобъекты A и B of D также являются базовыми подобъектами E, поэтому S(x,D)отбрасывается на первом этапе слияния. -end пример]

Итак, вот что происходит. Сначала мы попытаемся объединить пустой набор объявлений operator[] в D<float> с именем B<float>. Это дает нам набор {operator[](X)}.

Затем мы объединим это с набором объявлений operator[] в C<float>. Этот последний набор объявлений {operator[](Y)}. Эти слияния отличаются, поэтому слияние неоднозначно. Обратите внимание, что разрешение перегрузки не рассматривается здесь, Мы просто смотрим имя.

Исправление, кстати, заключается в том, чтобы добавить объявления-объявления в D<T>, чтобы не было сделано никакого шага слияния:

template<typename T> struct D : B<T>, C<T> {
    using B<T>::operator[];
    using C<T>::operator[];
};

Ответ 2

Это не 100% понятно, в ком компилятор проблема. Стандарт содержит множество правил для поиска имени (что и является проблемой), но более конкретно раздел 13.5.5 охватывает перегрузку operator[]:

13.5.5 Подписки [over.sub]

1 - operator[] должна быть нестатической функцией-членом с точно одним параметром. Он реализует синтаксис подписи

<я > postfix-expression [ expr-or-braced-init-list ]

Таким образом, выражение подписи x[y] интерпретируется как x.operator[](y) для объекта класса x типа T, если T::operator[](T1) существует и если оператор выбран как наилучшая функция соответствия механизмом разрешения перегрузки (13.3.3).

Взгляд на стандарт перегрузки (глава 13):

13 Перегрузка [более]

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

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

...

13.2 Согласование соответствия [over.dcl]

1 - Две объявления функций с тем же именем относятся к одной и той же функции, если они находятся в одной области действия и имеют эквивалентные объявления параметров (13.1). Член функции производного класса находится не в той же области, что и член функции с тем же именем в базовом классе.

Итак, согласно этому и разделу 10.2 на производных классах, поскольку вы объявили struct D : B, C, обе B и C имеют функции-члены для operator[], но разные типы, поэтому функция operator[] перегружена в рамках D (так как нет using и не operator[] переопределено или скрыто непосредственно в D).

Исходя из этого, MSVC и Clang неверны в своих реализациях, так как d[Y()] should оценивается до d.operator[](Y()), что вызывают неоднозначное разрешение имен; поэтому вопрос почему они принимают синтаксис d[Y()] вообще?

Единственные другие области, которые я мог видеть в отношении синтаксиса индекса ([]), ссылаются на раздел 5.2.1 (в котором указано, что такое индексное выражение) и 13.5. 5 (указано выше), что означает, что эти компиляторы используют другие правила для дальнейшей компиляции выражения d[Y()].

Если мы посмотрим на поиск имени, мы увидим, что 3.4.1. Проверка неквалифицированного имени гласит, что

Поиск неквалифицированного имени, используемого как постфиксное выражение вызова функции, описан в разделе 3.4.2.

Где 3.4.2 состояния:

3.4.2 Поиск зависимых от аргументов имен [basic.lookup.argdep]

1 - Когда постфиксное выражение в вызове функции (5.2.2) является неквалифицированным-id, другие пространства имен, которые не рассматриваются при обычном неквалифицированном поиске (3.4.1) , могут быть найдены и в этих пространствах имен функция поиска имени пространства-области или объявления шаблона шаблона (11.3) не видна иначе может.

2 - для каждого типа аргумента T в вызове функции существует множество нулевых или более связанных пространств имен и множество нулевых или более связанных классов, которые необходимо учитывать. Наборы пространств имен и классов полностью определяются типами аргументов функции (и пространства имен любого аргумента шаблона шаблона). Названия Typedef и используемые объявления-декларации, используемые для указания типов, не вносят вклад в этот набор. Наборы пространств имен и классов определяются следующим образом:

...

(2.2). Если T - тип класса (включая объединения), его ассоциированные классы: сам класс; класс которого он является членом, если таковой имеется; и его прямые и косвенные базовые классы. Его связанные пространства имен являются внутренними охватывающими пространствами имен связанных классов. Кроме того, если T - это специализированная спецификация шаблона, связанные с ним пространства имен и классы также включают: пространства имен и классы, связанные с типами аргументов шаблона, предоставленными для параметров типа шаблона (исключая параметры шаблона шаблона); пространства имен, членами которых являются аргументы шаблона шаблона; и классы, в которых любые шаблоны-члены, используемые в качестве аргументов шаблона шаблона, являются членами. [Примечание: аргументы шаблона непигового типа не вносят вклад в набор связанных пространств имен.-End note]

Обратите внимание, что акцент на может.

С приведенными выше пунктами и несколькими другими из 3.4 (поиск имени) можно было бы предположить, что Clang и MSVC используют эти правила, чтобы сначала найти d[] (и, таким образом, найти его как C::operator[]) против 13.5.5 превратить d[] в d.operator[] и продолжить компиляцию.

Следует отметить, что приведение операторов базовых классов в область класса D или использование явной области действия, однако, "исправляет" эту проблему для всех трех компиляторов (как ожидается, исходя из условий использования объявлений в ссылках), например:

struct X{};
struct Y{};

template<typename T>
struct B
{
    void f(X) { }
    void operator[](X) {}
};

template<typename T>
struct C
{
    void f(Y) { }
    void operator[](Y) {}
};

template<typename T>
struct D : B<T>, C<T>
{
    using B<T>::operator[];
    using C<T>::operator[];
};

int main()
{
    D<float> d;

    d.B<float>::operator[](X()); // OK
    //d.B<float>::operator[](Y()); // Error

    //d.C<float>::operator[](X()); // Error
    d.C<float>::operator[](Y()); // OK

    d[Y()]; // calls C<T>::operator[](Y)
    return 0;
}

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

Я надеюсь, что это может добавить некоторое понимание проблемы.