6.4. Перегруженные функции

Функции, расположенные в одной области видимости, называются перегруженными (overloaded), если они имеют одинаковые имена, но разные списки параметров. Пример определения нескольких функций по имени print() приведен в разделе 6.2.4:

void print(const char *cp);

void print(const int *beg, const int *end);

void print(const int ia[], size_t size);

Эти функции выполняют одинаковое действие, но их параметры относятся к разным типам. При вызове такой функции компилятор принимает решение о применении конкретной версии на основании типа переданного аргумента:

int j[2] = {0, 1};

print("Hello World");        // вызов print (const char*)

print(j, end(j) - begin(j)); // вызов print(const int*, size_t)

print(begin(j), end(j));     // вызов print(const int*, const int*)

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

Функция main() не может быть перегружена.

Определение перегруженных функций

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

Record lookup(const Account&); // поиск по счету

Record lookup(const Phone&);   // поиск по телефону

Record lookup(const Name&);    // поиск по имени

Account acct;

Phone phone;

Record r1 = lookup(acct);  // вызов версии, получающей Account

Record r2 = lookup(phone); // вызов версии, получающей Phone

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

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

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

Record lookup(const Account&);

bool lookup(const Account&); // ошибка: отличается только типом

                             // возвращаемого значения

Различие типов параметров

Два списка параметров могут быть идентичными, даже если они не выглядят одинаково:

// каждая пара объявляет ту же функцию

Record lookup(const Account &acct);

Record lookup(const Account&); // имена параметров игнорируются

typedef Phone Telno;

Record lookup(const Phone&);

Record lookup(const Telno&);   // Telno и Phone того же типа

Первое объявление в первой паре именует свой параметр. Имена параметров предназначены только для документирования. Они не изменяют список параметров.

Во второй паре типы только выглядят разными, Telno — не новый тип, это только синоним типа Phone. Псевдоним типа (см. раздел 2.5.1) предоставляет альтернативное имя для уже существующего типа, а не создает новый тип. Поэтому два параметра, отличающиеся только тем, что один использует имя типа, а другой его псевдоним, не являются разными.

Перегрузка и константные параметры

Как упоминалось в разделе 6.2.3, спецификатор const верхнего уровня (см. раздел 2.4.3) никак не влияет на объекты, которые могут быть переданы функции. Параметр, у которого есть спецификатор const верхнего уровня, неотличим от такового без спецификатора const верхнего уровня:

Record lookup(Phone);

Record lookup(const Phone);  // повторно объявляет Record lookup(Phone)

Record lookup(Phone*);

Record lookup(Phone* const); // повторно объявляет

                             // Record lookup(Phone*)

Здесь вторые объявления повторно объявляет ту же функцию, что и первые. С другой стороны, функцию можно перегрузить на основании того, является ли параметр ссылкой (или указателем) на константную или неконстантную версию того же типа; речь идет о спецификаторе const нижнего уровня:

// функции, получающие константную и неконстантную ссылку (или

// указатель), имеют разные параметры

Record lookup(Account&);       // функция получает ссылку на Account

Record lookup(const Account&); // новая функция получает константную

                               // ссылку

Record lookup(Account*);       // новая функция получает указатель

                               // на Account

Record lookup(const Account*); // новая функция получает указатель на

                               // константу

В этих случаях компилятор может использовать константность аргумента, чтобы различить, какую функцию применять. Поскольку нет преобразования (см. раздел 4.11.2) из константы, можно передать константный объект (или указатель на константу) только версии с константным параметром. Так как преобразование в константу возможно, можно вызвать функцию и неконстантного объекта, и указателя на неконстантный объект. Однако, как будет представлено в разделе 6.6.1, компилятор предпочтет неконстантные версии при передаче неконстантного объекта или указателя на неконстантный объект.

Совет. Когда не следует перегружать функции

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

Screen& moveHome();

Screen& moveAbs(int, int);

Screen& moveRel(int, int, string direction);

На первый взгляд может показаться, что этот набор функций имеет смысл перегрузить под именем move:

Screen& move();

Screen& move(int, int);

Screen& move(int, int, string direction);

Однако при перегрузке этих функций мы потеряли информацию, которая была унаследована именами функции. Хотя перемещение курсора — это общая операция, совместно используемая всеми этими функциями, специфический характер перемещения уникален для каждой из этих функций. Рассмотрим, например, функцию moveHome(), осуществляющую вполне определенное перемещение курсора. Какое из двух приведенных ниже обращений понятнее при чтении кода?

// которая из записей понятней?

myScreen.moveHome(); // вероятно, эта!

myScreen.move();

Оператор const_cast и перегрузка

В разделе 4.11.3 упоминалось, что оператор const_cast особенно полезен в контексте перегруженных функций. В качестве примера вернемся к функции shorterString() из раздела 6.3.2:

// возвратить ссылку на строку, которая короче

const string &shorterString (const string &s1, const string &s2) {

 return s1.size() <= s2.size() ? s1 : s2;

}

Эта функция получает и возвращает ссылки на константную строку. Мы можем вызвать функцию с двумя неконстантными строковыми аргументами, но как результат получим ссылку на константную строку. Могла бы понадобиться версия функции shorterString(), которая, получив неконстантные аргументы, возвратит обычную ссылку. Мы можем написать эту версию функции, используя оператор const_cast:

string &shorterString(string &s1, string &s2) {

 auto &r = shorterString(const_cast<const string&>(s1),

                         const_cast<const string&>(s2));

 return const_cast<string&>(r);

}

Эта версия вызывает константную версию функции shorterString() при приведении типов ее аргументов к ссылкам на константу. Функция возвращает ссылку на тип const string, которая, как известно, привязана к одному из исходных, неконстантных аргументов. Следовательно, приведение этой строки назад к обычной ссылке string& при возвращении вполне безопасно.

Вызов перегруженной функции

Когда набор перегруженных функций определен, необходима возможность вызвать их с соответствующими аргументами. Подбор функции (function matching), известный также как поиск перегруженной функции (overload resolution), — это процесс, в ходе которого вызов функции ассоциируется с определенной версией из набора перегруженных функций. Компилятор определяет, какую именно версию функции использовать при вызове, сравнивая аргументы вызова с параметрами каждой функции в наборе.

Как правило, вовсе несложно выяснить, допустим ли вызов, и если он допустим, то какая из версий функции будет использована компилятором. Функции в наборе перегруженных версий отличаются количеством или типом аргументов. В таких случаях определить используемую функцию просто. Подбор функции усложняется в случае, когда количество параметров одинаково и они допускают преобразование (см. раздел 4.11) переданных аргументов. Распознавание вызовов компилятором при наличии преобразований рассматривается в разделе 6.6, а пока следует понять, что при любом вызове перегруженной функции возможен один из трех результатов.

• Компилятор находит одну функцию, которая является наилучшим соответствием (best match) для фактических аргументов, и создает код ее вызова.

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

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

Упражнения раздела 6.4

Упражнение 6.39. Объясните результат второго объявления в каждом из следующих наборов. Укажите, какое из них (если есть) недопустимо.

(a) int calc(int, int);

    int calc(const int, const int);

(b) int get();

    double get();

(c) int *reset(int *);

    double *reset(double *);

Более 800 000 книг и аудиокниг! 📚

Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением

ПОЛУЧИТЬ ПОДАРОК