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

Разрешение перегрузки для унаследованного оператора()

Сначала рассмотрим этот код на С++:

#include <stdio.h>

struct foo_int {
    void print(int x) {
        printf("int %d\n", x);
    }    
};

struct foo_str {
    void print(const char* x) {
        printf("str %s\n", x);
    }    
};

struct foo : foo_int, foo_str {
    //using foo_int::print;
    //using foo_str::print;
};

int main() {
    foo f;
    f.print(123);
    f.print("abc");
}

Как и ожидалось в соответствии со стандартом, это не скомпилируется, потому что print рассматривается отдельно в каждом базовом классе с целью разрешения перегрузки, и, следовательно, вызовы неоднозначны. Это относится к Clang (4.0), gcc (6.3) и MSVC (17.0) - см. Результаты godbolt здесь.

Теперь рассмотрим следующий фрагмент, единственная разница которого заключается в том, что вместо print мы используем operator():

#include <stdio.h>

struct foo_int {
    void operator() (int x) {
        printf("int %d\n", x);
    }    
};

struct foo_str {
    void operator() (const char* x) {
        printf("str %s\n", x);
    }    
};

struct foo : foo_int, foo_str {
    //using foo_int::operator();
    //using foo_str::operator();
};

int main() {
    foo f;
    f(123);
    f("abc");
}

Я ожидаю, что результаты будут идентичны предыдущему случаю, но это не тот случай - в то время как gcc все еще жалуется, Clang и MSVC может скомпилировать этот штраф!

Вопрос №1: кто прав в этом случае? Я ожидаю, что это будет gcc, но тот факт, что два других несвязанных компилятора дают неизменно другой результат, заставляет меня задаться вопросом, не хватает ли я чего-то в стандарте, и все по-другому для операторов, когда они не вызываются с помощью синтаксиса функций.

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

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

#include <stdio.h>

auto print_int = [](int x) {
    printf("int %d\n", x);
};
typedef decltype(print_int) foo_int;

auto print_str = [](const char* x) {
    printf("str %s\n", x);
};
typedef decltype(print_str) foo_str;

struct foo : foo_int, foo_str {
    //using foo_int::operator();
    //using foo_str::operator();
    foo(): foo_int(print_int), foo_str(print_str) {}
};

int main() {
    foo f;
    f(123);
    f("foo");
}

Опять же, как и раньше, за исключением теперь мы не определяем operator() явно, а вместо этого получаем его из лямбда-типа. Опять же, вы ожидаете, что результаты будут соответствовать предыдущему фрагменту; и это справедливо для случая, когда объявления using закомментированы, или если оба без рапорта. Но если вы только прокомментируете одно, а не другое, вещи внезапно отличаются друг от друга: теперь только MSVC жалуется, как я ожидал, в то время как Clang и gcc оба считают, что это нормально - и использовать оба унаследованных элемента для разрешения перегрузки, несмотря на то, что только один из них привнесен в using!

Вопрос №2: кто прав в этом случае? Опять же, я ожидаю, что это будет MSVC, но почему же Clang и gcc не согласны? И что еще более важно, почему это отличается от предыдущего фрагмента? Я бы ожидал, что лямбда-тип будет вести себя точно так же, как с заданным вручную типом с перегруженным operator()...

4b9b3361

Ответ 1

Барри получил № 1 вправо. Ваш №2 попал в угловой случай: беззамадные негерметичные лямбды имеют неявное преобразование в указатель функций, который использовался в случае несоответствия. То есть, учитывая
struct foo : foo_int, foo_str {
    using foo_int::operator();
    //using foo_str::operator();
    foo(): foo_int(print_int), foo_str(print_str) {}
} f;

using fptr_str = void(*)(const char*);

f("hello") эквивалентен f.operator fptr_str()("hello"), преобразуя foo в указатель на функцию и вызывая это. Если вы компилируете в -O0, вы можете увидеть вызов функции преобразования в сборке, прежде чем она будет оптимизирована. Поместите init-capture в print_str, и вы увидите ошибку, поскольку неявное преобразование уходит.

Подробнее см. [over.call.object].

Ответ 2

Правило для поиска по именам в базовых классах класса C происходит только в том случае, если C непосредственно не содержит имя [class.member.lookup]/6:

Следующие шаги определяют результат слияния набора поиска S(f,Bi) в промежуточный S(f,C):

  • Если каждый из подобъектных элементов S (f, Bi) является подобъектом базового класса хотя бы одного из субобъектов S (f, C) или если S (f, Bi) пуст, S (f, C) не изменяется и слияние завершено. И наоборот, если каждый из субобъектов S (f, C) является подобъектом базового класса хотя бы одного из субобъектов S (f, Bi) или если S (f, C) пуст, то новый S (f, C) является копией S (f, Bi).

  • В противном случае, если множества объявлений S (f, Bi) и S (f, C) различаются, слияние неоднозначно: новый S (f, C) является lookup set с недопустимым набором объявлений и объединением наборов подобъектов. В последующих слияниях недопустимый набор объявлений считается отличным от любого другого.

  • В противном случае новый S (f, C) является поисковым набором с общим набором объявлений и объединением наборов подобъектов.

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

Вопрос №1: кто прав в этом случае?

gcc правильный. Единственное различие между print и operator() - это имя, которое мы просматриваем.

Вопрос №2: кто прав в этом случае?

Это тот же вопрос, что и # 1, за исключением того, что мы имеем lambdas (который дает вам типы неназванных классов с перегрузкой operator()) вместо явных типов классов. По той же причине код должен быть плохо сформирован. По крайней мере, для gcc это ошибка 58820.

Ответ 3

Ваш анализ первого кода неверен. Нет разрешения перегрузки.

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

Если уникальная область видимости найдена с помощью правил поиска имен, тогда начинается разрешение перегрузки: все экземпляры этого имени в этой области образуют набор перегрузки.

Но в вашем коде поиск имени не выполняется. Имя не объявляется в foo, поэтому базовые классы выполняются. Если имя найдено более чем в одном ближайшем базовом классе, программа плохо сформирована, и сообщение об ошибке описывает это как неоднозначное имя.


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

f.operator()(123);

не удается по той же причине, что и f.print. Однако во втором коде есть еще одна проблема. f(123) НЕ определяется как всегда значение f.operator()(123);. На самом деле определение в С++ 14 находится в [over.call]:

operator() должна быть нестатической функцией-членом с произвольным числом параметров. Он может иметь аргументы по умолчанию. Он реализует синтаксис вызова функции

postfix-expression (выражение-list opt)

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

Это на самом деле кажется неточной спецификацией для меня, поэтому я могу понять, что разные компиляторы выходят с разными результатами. Что такое T1, T2, T3? Означает ли это типы аргументов? (Я не подозреваю). Что такое T1, T2, T3, когда существует несколько функций operator(), только принимая один аргумент?

И что означает "если T::operator() существует"? Это может означать любое из следующего:

  • operator() объявлен в T.
  • Неквалифицированный поиск operator() в области T преуспевает и выполнение разрешения перегрузки в этом наборе поиска с заданными аргументами завершается успешно.
  • Квалифицированный поиск T::operator() в вызывающем контексте преуспевает и выполнение разрешения перегрузки в этом наборе поиска с заданными аргументами выполняется успешно.
  • Что-то еще?

Чтобы исходить отсюда (для меня в любом случае), я хотел бы понять, почему в стандарте не просто говорилось, что f(123) означает f.operator()(123);, первый из них плохо сформирован тогда и только тогда, когда последний плохо сформирован, Мотивация фактической формулировки может показать намерение и, следовательно, поведение компилятора соответствует намерению.