Как работает разрешение перегрузки, когда аргумент является перегруженной функцией?

преамбула

Разрешение перегрузки в C++ может быть слишком сложным процессом. Требуется немало умственных усилий, чтобы понять все правила C++, которые управляют разрешением перегрузки. Недавно мне пришло в голову, что наличие имени перегруженной функции в списке аргументов может увеличить сложность разрешения перегрузки. Поскольку это оказалось широко используемым случаем, я разместил вопрос и получил ответ, который позволил мне лучше понять механику этого процесса. Однако формулировка этого вопроса в контексте iostreams, похоже, несколько отвлекла внимание от самой сути решаемой проблемы. Таким образом, я начал углубляться и нашел другие примеры, которые требуют более детального анализа проблемы. Этот вопрос является вводным и сопровождается более сложным.

Вопрос

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

Примеры

Учитывая эти заявления:

void foo(int) {}
void foo(double) {}
void foo(std::string) {}
template<class T> void foo(T* ) {}

struct A {
    A(void (*)(int)) {}
};

void bar(int x, void (*f)(int)) {}
void bar(double x, void (*f)(double)) {}
void bar(std::string x, void (*f)(std::string)) {}
template<class T> void bar(T* x, void (*f)(T*)) {}
void bar(A x, void (*f2)(double)) {}

Приведенные ниже выражения приводят к следующему разрешению имени foo (хотя бы с gcc 5.4):

bar(1, foo); // foo(int)
             // but if foo(int) is removed, foo(double) takes over

bar(1.0, foo); // foo(double)
               // but if foo(double) is removed, foo(int) takes over

int i;
bar(&i, foo); // foo<int>(int*)

bar("abc", foo); // foo<const char>(const char*)
                 // but if foo<T>(T*) is removed, foo(std::string) takes over

bar(std::string("abc"), foo); // foo(std::string)

bar(foo, foo); // 1st argument is foo(int), 2nd one - foo(double)

Код для игры:

#include <iostream>
#include <string>

#define PRINT_FUNC  std::cout << "\t" << __PRETTY_FUNCTION__ << "\n";

void foo(int)                      { PRINT_FUNC; }
void foo(double)                   { PRINT_FUNC; }
void foo(std::string)              { PRINT_FUNC; }
template<class T> void foo(T* )    { PRINT_FUNC; }

struct A { A(void (*f)(int)){ f(0); } };

void bar(int         x, void (*f)(int)        ) { f(x); }
void bar(double      x, void (*f)(double)     ) { f(x); }
void bar(std::string x, void (*f)(std::string)) { f(x); }
template<class T> void bar(T* x, void (*f)(T*)) { f(x); }
void bar(A, void (*f)(double)) { f(0); }

#define CHECK(X) std::cout << #X ":\n"; X; std::cout << "\n";

int main()
{
    int i = 0;
    CHECK( bar(i, foo)                     );
    CHECK( bar(1.0, foo)                   );
    CHECK( bar(1.0f, foo)                  );
    CHECK( bar(&i, foo)                    );
    CHECK( bar("abc", foo)                 );
    CHECK( bar(std::string("abc"), foo)    );
    CHECK( bar(foo, foo)                   );
}

1 ответ

Решение

Давайте возьмем самый интересный случай,

bar("abc", foo);

Основной вопрос, который нужно выяснить, - какая перегрузка bar использовать. Как всегда, мы сначала получаем набор перегрузок с помощью поиска по имени, затем делаем вывод типа шаблона для каждого шаблона функции в наборе перегрузки, затем выполняем разрешение перегрузки.

Действительно интересная часть - это вычет типа шаблона для объявления

template<class T> void bar(T* x, void (*f)(T*)) {}

Стандарт говорит следующее в 14.8.2.1/6:

когда P тип функции, указатель на тип функции или указатель на тип функции-члена:

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

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

(P уже определен как тип параметра функции шаблона функции, включая параметры шаблона, поэтому здесь P является void (*)(T*).)

Так как foo является набором перегрузки, содержащим шаблон функции, foo а также void (*f)(T*) не играют роли в выводе типа шаблона. Это оставляет параметр T* x и аргумент "abc" с типом const char[4], T* не являясь ссылкой, тип массива распадается на тип указателя const char* и мы находим, что T является const char,

Теперь у нас есть разрешение перегрузки с этими кандидатами:

void bar(int x, void (*f)(int)) {}                             // (1)
void bar(double x, void (*f)(double)) {}                       // (2)
void bar(std::string x, void (*f)(std::string)) {}             // (3)
void bar<const char>(const char* x, void (*f)(const char*)) {} // (4)
void bar(A x, void (*f2)(double)) {}                           // (5)

Пора выяснить, какие из них являются жизнеспособными функциями. (1), (2) и (5) не являются жизнеспособными, потому что нет преобразования из const char[4] в int, double, или же A, Для (3) и (4) нам нужно выяснить, если foo является действительным вторым аргументом. В стандартном разделе 13.4/1-6:

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

  • ...
  • параметр функции (5.2.2),
  • ...

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

[Примечание: если f() а также g() обе перегруженные функции, перекрестный продукт возможностей должен быть рассмотрен для разрешения f(&g)или эквивалентное выражение f(g), - конец примечания]

Для перегрузки (3) из barмы сначала пытаемся вычесть типа для

template<class T> void foo(T* ) {}

с целевым типом void (*)(std::string), Это не с тех пор std::string не может соответствовать T*, Но мы находим одну перегрузку foo который имеет точный тип void (std::string)Таким образом, он выигрывает в случае перегрузки (3), и перегрузка (3) является жизнеспособной.

Для перегрузки (4) из barмы сначала пытаемся вывести тип для того же шаблона функции fooна этот раз с целевым типом void (*)(const char*) На этот раз вывод типа T знак равно const char, Ни одна из других перегрузок foo иметь точный тип void (const char*), поэтому используется специализация шаблона функции, и перегрузка (4) является жизнеспособной.

Наконец, мы сравниваем перегрузки (3) и (4) обычным разрешением перегрузки. В обоих случаях преобразование аргумента foo указатель на функцию является точным соответствием, поэтому ни одна неявная последовательность преобразования не лучше, чем другая. Но стандартное преобразование из const char[4] в const char* лучше, чем пользовательская последовательность преобразования из const char[4] в std::string, Так что перегрузка (4) bar это лучшая жизнеспособная функция (и она использует void foo<const char>(const char*) в качестве аргумента).

Другие вопросы по тегам