Разрешение перегрузки для унаследованного оператора()
Сначала рассмотрим этот код на С++:
#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()
...
Ответы
Ответ 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);
, первый из них плохо сформирован тогда и только тогда, когда последний плохо сформирован, Мотивация фактической формулировки может показать намерение и, следовательно, поведение компилятора соответствует намерению.