← Назад к вопросам

Как компилятор выбирает функцию из списка доступных перегрузок?

2.0 Middle🔥 191 комментариев
#Linux и операционные системы#Qt и GUI#STL контейнеры и алгоритмы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Разрешение перегрузок функций в C++

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

Три этапа разрешения перегрузок

Этап 1: Построение набора кандидатов (Candidate Set)

Компилятор ищет все функции с тем же именем в области видимости:

void print(int x);        // Кандидат 1
void print(double x);     // Кандидат 2
void print(const char* x); // Кандидат 3

void print(int x, int y); // НЕ кандидат (другое количество параметров)

print(42);  // Начинаем с набора из 3 кандидатов

Этап 2: Фильтрация по применимости (Viable Functions)

Из кандидатов оставляем только те, которые могут быть вызваны:

void print(int x);
void print(double x);

print(42);      // Оба применимы
print(42.5);    // Оба применимы (int через неявное преобразование)
print("hello"); // НИ ОДИН не применим (ошибка компиляции)

Этап 3: Выбор лучшей функции (Best Viable Function)

Из применимых функций выбирается лучшая по определённым правилам.

Степени преобразования (Conversion Ranks)

Компилятор ранжирует преобразования типов:

1. Точное совпадение (Exact Match) — самое предпочтительное

void print(int x);
print(42);  // Точное совпадение, выбирается сразу

2. Повышение типа (Promotion)

void print(int x);
void print(double x);
print('A');  // char повышается до int (лучше, чем до double)
            // Выбирается print(int)

3. Стандартное преобразование (Standard Conversion)

void print(int x);
void print(const char* x);
print(42.7);  // double преобразуется в int (худше, чем promotion)
             // Выбирается print(int)

4. Определённое пользователем преобразование (User-defined Conversion)

class Integer {
public:
    Integer(int x) {}  // Конструктор преобразования
};

void print(Integer x);
print(42);  // Использует конструктор Integer(42)

Примеры разрешения перегрузок

Пример 1: Простая перегрузка

void foo(int x) { std::cout << "int" << std::endl; }
void foo(double x) { std::cout << "double" << std::endl; }

int main() {
    foo(42);     // Точное совпадение → foo(int)
    foo(42.5);   // double совпадает точно → foo(double)
    foo(42u);    // unsigned int. Нужно преобразование. double лучше? Нет!
                 // В C++, unsigned преобразуется в int → foo(int)
}

Пример 2: const/volatile

void func(int x);           // 1
void func(const int x);     // 2 (эквивалентно для by-value)

func(42);  // Точное совпадение для обоих!
           // Ошибка: амбигуозная перегрузка!

Но с указателями это работает:

void func(int* x);           // 1
void func(const int* x);     // 2 (явно разные)

int val = 42;
func(&val);     // int* совпадает точно → func(int*)

const int cval = 42;
func(&cval);    // const int* совпадает точно → func(const int*)

Пример 3: Ссылки

void func(int& x);           // 1
void func(const int& x);     // 2

int val = 42;
func(val);      // Точное совпадение → func(int&)

const int cval = 42;
func(cval);     // Точное совпадение → func(const int&)

func(42);       // Временное. Только const int& может привязать
                // → func(const int&)

Амбигуозные перегрузки

Иногда компилятор не может выбрать одну функцию:

void func(int x, double y);
void func(double x, int y);

func(42, 42);  // ОШИБКА: амбигуозно!
               // Обе требуют одного преобразования
               // Компилятор не знает, какую выбрать

Решение: явное приведение типов

func(static_cast<int>(42), static_cast<double>(42));

Специализация шаблонов (Templates)

С шаблонами разрешение перегрузок усложняется:

template<typename T>
void print(T x) { std::cout << "template<T>" << std::endl; }

void print(int x) { std::cout << "int" << std::endl; }

print(42);      // Точное совпадение для обычной функции
                // → print(int)

print(42.5);    // Обычная функция требует преобразования
                // Шаблон подходит точно (T=double)
                // → print(double)

Правило: обычная функция предпочтительнее, чем специализация шаблона.

Порядок лучших кандидатов

// 1. Точное совпадение в функции (не шаблон)
void func(int x);

// 2. Точное совпадение в специализации шаблона
template<>
void func<int>(int x);

// 3. Точное совпадение в общем шаблоне
template<typename T>
void func(T x);

// 4. С преобразованиями (лучше в общей функции)

Преобразования в параметрах

void func(char x);         // 1
void func(const char* x);  // 2

func("hello");  // string literal — const char*
                // Точное совпадение для func(const char*)
                // → func(const char*)

func('A');      // char
                // Точное совпадение для func(char)
                // → func(char)

Сложный пример: множественные параметры

void foo(int, int);          // 1
void foo(double, double);    // 2
void foo(int, double);       // 3
void foo(double, int);       // 4

foo(42, 42);      // Точное совпадение → foo(int, int)
foo(42.5, 42.5);  // Точное совпадение → foo(double, double)
foo(42, 42.5);    // Точное совпадение → foo(int, double)
foo(42.5, 42);    // Точное совпадение → foo(double, int)

Алгоритм выбора (упрощённый)

1. Найти все функции с тем же именем → Candidate Set
2. Отфильтровать по количеству параметров → Viable Set
3. Для каждого кандидата:
   - Вычислить "стоимость" преобразования каждого параметра
   - Если есть параметры, которые не преобразуются → исключить
4. Выбрать кандидата с МИНИМАЛЬНОЙ стоимостью
5. Если несколько кандидатов с одинаковой стоимостью → ошибка: амбигуозность

Частые ошибки

Ошибка 1: Неточные преобразования

void func(const char* x);

func(std::string("hello"));  // std::string → const char*?
                            // Требует вызова .c_str()
                            // Не автоматическое преобразование!

Ошибка 2: Амбигуозность с наследованием

class Base {};
class Derived : public Base {};

void func(Base b);
void func(Derived d);

Derived d;
func(d);  // Точное совпадение → func(Derived)

Base b = Derived();
func(b);  // Точное совпадение → func(Base)

Лучшие практики

// ✅ Избегай амбигуозных перегрузок
void func(int x);
void func(unsigned x);  // Потенциально амбигуозно

// ✅ Используй явные типы
void func(int x);
void func(double x);  // Явное разделение

// ✅ Помни про const и ссылки
void modify(int& x);       // Только для неconst ссылок
void process(const int& x); // Для const ссылок

// ✅ С шаблонами: специализируй явно
template<typename T>
void print(T x) {}

template<>
void print(int x) {}  // Явная специализация

Итоговый ответ

Компилятор выбирает перегрузку функции через:

  1. Построение набора кандидатов по имени
  2. Фильтрацию по применимости (тип параметров)
  3. Ранжирование по качеству преобразований
  4. Выбор лучшей функции
  5. Ошибка при амбигуозности

Порядок предпочтения: точное совпадение > повышение типа > стандартное преобразование > определённое пользователем преобразование > ошибка.