Итак, мы уже знаем, как объявлять, определять и использовать функции в программах. В этой главе речь пойдет об их специальном виде – перегруженных функциях. Две функции называются перегруженными, если они имеют одинаковое имя, объявлены в одной и той же области видимости, но имеют разные списки формальных параметров. Мы расскажем, как объявляются такие функции и почему они полезны. Затем мы рассмотрим вопрос об их разрешении, т.е. о том, какая именно из нескольких перегруженных функций вызывается во время выполнения программы. Эта проблема является одной из наиболее сложных в C++. Тем, кто хочет разобраться в деталях, будет интересно прочитать два раздела в конце главы, где тема преобразования типов аргументов и разрешения перегруженных функций раскрывается более подробно.
§ 1.1. Объявления перегруженных функций
§ 1.2. Три шага разрешения перегрузки
§ 1.3. Преобразования типов аргументов
§ 1.4. Детали разрешения перегрузки функций
Теперь, научившись объявлять, определять и использовать функции в программах, познакомимся с перегрузкой – еще одним аспектом в C++. Перегрузка позволяет иметь несколько одноименных функций, выполняющих схожие операции над аргументами разных типов.
Вы уже воспользовались предопределенной перегруженной функцией. Например, для вычисления выражения
1 + 3
вызывается операция целочисленного сложения, тогда как вычисление выражения
1.0 + 3.0
осуществляет сложение с плавающей точкой. Выбор той или иной операции производится незаметно для пользователя. Операция сложения перегружена, чтобы обеспечить работу с операндами разных типов. Ответственность за распознавание контекста и применение операции, соответствующей типам операндов, возлагается на компилятор, а не на программиста.
В этой главе мы покажем, как определять собственные перегруженные функции.
Как и в случае со встроенной операцией сложения, нам может понадобиться набор функций, выполняющих одно и то же действие, но над параметрами различных типов. Предположим, что мы хотим определить функции, возвращающие наибольшее из переданных значений параметров. Если бы не было перегрузки, пришлось бы каждой такой функции присвоить уникальное имя. Например, семейство функций max() могло бы выглядеть следующим образом:
int i_max(int, int); int vi_max(const std::vector<int>&); int matrix_max(const matrix&);
Однако все они делают одно и то же: возвращают наибольшее из значений параметров. С точки зрения пользователя, здесь лишь одна операция – вычисление максимума, а детали ее реализации большого интереса не представляют.
Отмеченная лексическая сложность отражает ограничение программной среды: всякое имя, встречающееся в одной и той же области видимости, должно относиться к уникальной сущности (объекту, функции, классу и т.д.). Такое ограничение на практике создает определенные неудобства, поскольку программист должен помнить или каким-то образом отыскивать все имена. Перегрузка функций помогает справиться с этой проблемой.
Применяя перегрузку, программист может написать примерно так:
int ix = max(j, k); std::vector<int> vec; //... int iy = max(vec);
Этот подход оказывается чрезвычайно полезным во многих ситуациях.
В C++ двум или более функциям может быть дано одно и то же имя при условии, что их списки параметров различаются либо числом параметров, либо их типами. В данном примере мы объявляем перегруженную функцию max():
int max (int, int); int max(const std::vector<int>&); int max(const matrix&);
Для каждого перегруженного объявления требуется отдельное определение функции max() с соответствующим списком параметров. Если в некоторой области видимости имя функции объявлено более одного раза, то второе (и последующие) объявление интерпретируется компилятором так:
// перегруженные функции void print(const string&); void print(std::vector<int>&);
// объявления одной и той же функции void print(const string& str); void print(const string&);
unsigned int max(int i1, int i2); int max(int i1, int i2); // ошибка: отличаются только типы // возвращаемых значений
Перегруженные функции не могут различаться лишь типами возвращаемого значения; если списки параметров двух функций разнятся только подразумеваемыми по умолчанию значениями аргументов, то второе объявление считается повторным:
// объявления одной и той же функции int max (int *ia, int sz); int max (int *ia, int = 10);
Ключевое слово typedef создает альтернативное имя для существующего типа данных, новый тип при этом не создается. Поэтому если списки параметров двух функций различаются только тем, что в одном используется typedef, а в другом тип, для которого typedef служит псевдонимом, такие списки считаются одинаковыми, как, например, в следующих двух объявлениях функции calc(). В таком случае второе объявление даст ошибку компиляции, поскольку возвращаемое значение отличается от указанного раньше:
// typedef не вводит нового типа typedef double DOLLAR; // ошибка: одинаковые списки параметров, но разные типы // возвращаемых значений extern DOLLAR calc(DOLLAR); extern int calc(double);
Спецификаторы const или volatile при подобном сравнении не принимаются во внимание. Так, следующие два объявления считаются одинаковыми:
// объявляют одну и ту же функцию void f(int); void f(const int);
Спецификатор const важен только внутри определения функции: он показывает, что в теле функции запрещено изменять значение параметра. Однако аргумент, передаваемый по значению, можно использовать в теле функции как обычную инициированную переменную: вне функции изменения не видны. Добавление спецификатора const к параметру, передаваемому по значению, не влияет на его интерпретацию. Функции, объявленной как f(int), может быть передано любое значение типа int, равно как и функции f(const int). Поскольку они обе принимают одно и то же множество значений аргумента, то приведенные объявления не считаются перегруженными. f() можно определить как
void f(int i) { }
или как
void f(const int i) { }
Наличие двух этих определений в одной программе – ошибка, так как одна и та же функция определяется дважды. Однако, если спецификатор const или volatile применяется к параметру указательного или ссылочного типа, то при сравнении объявлений он учитывается.
// объявляются разные функции void f(int*); void f(const int*); // и здесь объявляются разные функции void f(int&); void f(const int&);
В каких случаях перегрузка имени не дает преимуществ? Например, тогда, когда присвоение функциям разных имен облегчает чтение программы. Вот несколько примеров. Следующие функции оперируют одним и тем же абстрактным типом даты. На первый взгляд, они являются подходящими кандидатами для перегрузки:
void setDate(Date&, int, int, int); Date& convertDate(const string&); void printDate(const Date&);
Эти функции работают с одним типом данных – классом Date, но выполняют семантически различные действия. В этом случае лексическая сложность, связанная с употреблением различных имен, проистекает из принятого программистом соглашения об обеспечении набора операций над типом данных и именования функций в соответствии с семантикой этих операций. Правда, механизм классов C++ делает такое соглашение излишним. Следовало бы сделать такие функции членами класса Date, но при этом оставить разные имена, отражающие смысл операции:
#include <string> class Date { public: set(int, int, int); Date& convert(const string&); void print(); // ... };
Приведем еще один пример. Следующие пять функций-членов Screen выполняют различные операции над экранным курсором, являющимся принадлежностью того же класса. Может показаться, что разумно перегрузить эти функции под общим названием move():
Screen& moveHome(); Screen& moveAbs(int, int); Screen& moveRel(int, int, char *direction); Screen& moveX(int); Screen& moveY(int);
Впрочем, последние две функции перегрузить нельзя, так как у них одинаковые списки параметров. Чтобы сделать сигнатуру уникальной, объединим их в одну функцию:
// функция, объединяющая moveX() и moveY() Screen& move(int, char xy);
Теперь у всех функций разные списки параметров, так что их можно перегрузить под именем move(). Однако этого делать не следует: разные имена несут информацию, без которой программу будет труднее понять. Так, выполняемые данными функциями операции перемещения курсора различны. Например, moveHome() осуществляет специальный вид перемещения в левый верхний угол экрана. Какой из двух приведенных ниже вызовов более понятен пользователю и легче запоминается?
// какой вызов понятнее? myScreen.home(); // мы считаем, что этот! myScreen.move();
В некоторых случаях не нужно ни перегружать имя функции, ни назначать разные имена: применение подразумеваемых по умолчанию значений аргументов позволяет объединить несколько функций в одну. Например, функции управления курсором
moveAbs(int, int); moveAbs(int, int, char*);
различаются наличием третьего параметра типа char*. Если их реализации похожи и для третьего аргумента можно найти разумное значение по умолчанию, то обе функции можно заменить одной. В данном случае на роль значения по умолчанию подойдет указатель со значением 0:
move(int, int, char* = 0);
Применять те или иные возможности следует тогда, когда этого требует логика приложения. Вовсе не обязательно включать перегруженные функции в программу только потому, что они существуют.
Все перегруженные функции объявляются в одной и той же области видимости. К примеру, локально объявленная функция не перегружает, а просто скрывает глобальную:
#include <string> void print(const string &); void print(double); // перегружает print() void fooBar(int ival) { // отдельная область видимости: скрывает обе реализации print() extern void print(int); // ошибка: print(const string &) не видна в этой области print("Value: "); print(ival); // правильно: print(int) видна }
Поскольку каждый класс определяет собственную область видимости, функции, являющиеся членами двух разных классов, не перегружают друг друга. (Функции-члены класса описываются в главе 13. Разрешение перегрузки для функций-членов класса рассматривается в главе 15.)
Объявлять такие функции разрешается и внутри пространства имен. С каждым из них также связана отдельная область видимости, так что функции, объявленные в разных пространствах, не перегружают друг друга. Например:
#include <string> namespace IBM { extern void print(const string&); extern void print(double); // перегружает print() } namespace Disney { // отдельная область видимости: // не перегружает функцию print() из пространства имен IBM extern void print(int); }
Использование using-объявлений и using-директив помогает сделать члены пространства имен доступными в других областях видимости. Эти механизмы оказывают определенное влияние на объявления перегруженных функций.
Каким образом using-объявление сказывается на перегрузке функций? Напомним, что оно вводит псевдоним для члена пространства имен в ту область видимости, в которой это объявление встречается. Что делают такие объявления в следующей программе?
namespace libs_R_us { int max(int, int); int max(double, double); extern void print(int); extern void print(double); } // using-объявления using libs_R_us::max; using libs_R_us::print(double); // ошибка void func() { max(87, 65); // вызывает libs_R_us::max(int, int) max(35.5, 76.6); // вызывает libs_R_us::max(double, double) }
Первое using-объявление вводит обе функции libs_R_us::max в глобальную область видимости. Теперь любую из функций max() можно вызвать внутри func(). По типам аргументов определяется, какую именно функцию вызывать. Второе using-объявление – это ошибка: в нем нельзя задавать список параметров. Функция libs_R_us::print() объявляется только так:
using libs_R_us::print;
Using-объявление всегда делает доступными все перегруженные функции с указанным именем. Такое ограничение гарантирует, что интерфейс пространства имен libs_R_us не будет нарушен. Ясно, что в случае вызова
print(88);
автор пространства имен ожидает, что будет вызвана функция libs_R_us::print(int). Если разрешить пользователю избирательно включать в область видимости лишь одну из нескольких перегруженных функций, то поведение программы становится непредсказуемым.
Что происходит, если using-объявление вводит в область видимости функцию с уже существующим именем? Эти функции выглядят так, как будто они объявлены прямо в том месте, где встречается using-объявление. Поэтому введенные функции участвуют в процессе разрешения имен всех перегруженных функций, присутствующих в данной области видимости:
#include <string> namespace libs_R_us { extern void print(int); extern void print(double); } extern void print(const string&); // libs_R_us::print(int) и libs_R_us::print(double) // перегружают print(const string &) using libs_R_us::print; void fooBar(int ival) { print("Value: "); // вызывает глобальную функцию // print(const string &) print(ival); // вызывает libs_R_us::print(int) }
Using-объявление добавляет в глобальную область видимости два объявления: для print(int) и для print(double). Они являются псевдонимами в пространстве libs_R_us и включаются в множество перегруженных функций с именем print, где уже находится глобальная print(const string&). При разрешении перегрузки print в fooBar рассматриваются все три функции.
Если using-объявление вводит некоторую функцию в область видимости, в которой уже имеется функция с таким же именем и таким же списком параметров, это считается ошибкой. С помощью using-объявления нельзя задать псевдоним для функции print(int) в пространстве имен libs_R_us, если в глобальной области видимости уже есть print(int). Например:
namespace libs_R_us { void print(int); void print(double); } void print(int); using libs_R_us::print; // ошибка: повторное объявление print(int) void fooBar(int ival) { print(ival); // какая print? ::print или libs_R_us::print }
Мы показали, как связаны using-объявления и перегруженные функции. Теперь рассмотрим особенности применения using-директивы. Using-директива приводит к тому, что члены пространства имен выглядят объявленными вне этого пространства, добавляя их в новую область видимости. Если в этой области уже есть функция с тем же именем, то происходит перегрузка. Например:
#include <string> namespace libs_R_us { extern void print(int); extern void print(double); } extern void print(const string &); // using-директива // print(int), print(double) и print(const string &) - элементы // одного и того же множества перегруженных функций using namespace libs_R_us; void fooBar(int ival) { print("Value: "); // вызывает глобальную функцию // print(const string &) print(ival); // вызывает libs_R_us::print(int) }
Это верно и в том случае, когда есть несколько using-директив. Одноименные функции, являющиеся членами разных пространств, включаются в одно и то множество:
namespace IBM { int print(int); } namespace Disney { double print(double); } // using-директива // формируется множество перегруженных функций из различных // пространств имен using namespace IBM; using namespace Disney; long double print(long double); int main() { print(1); // вызывается IBM::print(int) print(3.1); // вызывается Disney::print(double) return 0; }
Множество перегруженных функций с именем print в глобальной области видимости включает функции print(int), print(double) и print(long double). Все они рассматриваются в main() при разрешении перегрузки, хотя первоначально были определены в разных пространствах имен.
Итак, повторим, что перегруженные функции находятся в одной и той же области видимости. В частности, они оказываются там в результате применения using-объявлений и using-директив, делающих доступными имена из других областей.
Директиву связывания extern "C" можно использовать в программе на C++ для того, чтобы указать, что некоторый объект находится в части, написанной на языке C. Как эта директива влияет на объявления перегруженных функций? Могут ли в одном и том же множестве находиться функции, написанные как на C++, так и на C?
В директиве связывания разрешается задать только одну из множества перегруженных функций. Например, следующая программа некорректна:
// ошибка: для двух перегруженных функций указана директива extern "C" extern "C" void print(const char*); extern "C" void print(int);
Приведенный ниже пример перегруженной функции calc() иллюстрирует типичное применение директивы extern "C":
class SmallInt (/* ... */); class BigNum (/* ... */); // написанная на C функция может быть вызвана как из программы, // написанной на C, так и из программы, написанной на C++. // функции C++ обрабатывают параметры, являющиеся классами extern "C" double calc(double); extern SmallInt calc(const SmallInt&); extern BigNum calc(const BigNum&);
Написанная на C функция calc() может быть вызвана как из C, так и из программы на C++. Остальные две функции принимают в качестве параметра класс и, следовательно, их допустимо использовать только в программе на C++. Порядок следования объявлений несуществен.
Директива связывания не имеет значения при решении, какую функцию вызывать; важны только типы параметров. Выбирается та функция, которая лучше всего соответствует типам переданных аргументов:
Smallint si = 8; int main() { calc(34); // вызывается C-функция calc(double) calc(si); // вызывается функция C++ calc(const SmallInt&) // ... return 0; }
Можно объявить указатель на одну из множества перегруженных функций. Например:
extern void ff(std::vector<double>); extern void ff(unsigned int); // на какую функцию указывает pf1? void (*pf1)(unsigned int) = &ff;
Поскольку функция ff() перегружена, одного инициализатора &ff недостаточно для выбора правильного варианта. Чтобы понять, какая именно функция инициализирует указатель, компилятор ищет в множестве всех перегруженных функций ту, которая имеет тот же тип возвращаемого значения и список параметров, что и функция, на которую ссылается указатель. В нашем случае будет выбрана функция ff(unsigned int).
А что если не найдется функции, в точности соответствующей типу указателя? Тогда компилятор выдаст сообщение об ошибке:
extern void ff(std::vector<double>); extern void ff(unsigned int);
// ошибка: соответствие не найдено: неверный список параметров void (*pf2)(int) = &ff; // ошибка: соответствие не найдено: неверный тип возвращаемого значения double (*pf3)(std::vector<double>) = &ff;
Присваивание работает аналогично. Если значением указателя должен стать адрес перегруженной функции , то для выбора операнда в правой части оператора присваивания используется тип указателя на функцию. И если компилятор не находит функции, в точности соответствующей нужному типу, он выдает сообщение об ошибке. Таким образом, преобразование типов между указателями на функции никогда не производится.
matrix calc(const matrix&); int calc(int, int); int (*pc1)(int, int) = 0; int (*pc2)(int, double) = 0; // ... // правильно: выбирается функция calc(int, int) pc1 = &calc; // ошибка: нет соответствия: неверный тип второго параметра pc2 = &calc;
При использовании перегрузки складывается впечатление, что в программе можно иметь несколько одноименных функций с разными списками параметров. Однако это лексическое удобство существует только на уровне исходного текста. В большинстве систем компиляции программы, обрабатывающие этот текст для получения исполняемого кода, требуют, чтобы все имена были различны. Редакторы связей, как правило, разрешают внешние ссылки лексически. Если такой редактор встречает имя print два или более раз, он не может различить их путем анализа типов (к этому моменту информация о типах обычно уже потеряна). Поэтому он просто печатает сообщение о повторно определенном символе print и завершает работу.
Чтобы разрешить эту проблему, имя функции вместе с ее списком параметров декорируется так, чтобы получилось уникальное внутреннее имя. Вызываемые после компилятора программы видят только это внутреннее имя. Как именно производится такое преобразование имен, зависит от реализации. Общая идея заключается в том, чтобы представить число и типы параметров в виде строки символов и дописать ее к имени функции.
Такое кодирование гарантирует, в частности, что два объявления одноименных функций с разными списками параметров, находящиеся в разных файлах, не воспринимаются редактором связей как объявления одной и той же функции. Поскольку этот способ помогает различить перегруженные функции на фазе редактирования связей, мы говорим о безопасном связывании.
Декорирование имен не применяется к функциям, объявленным с помощью директивы extern "C", так как лишь одна из множества перегруженных функций может быть написана на чистом С. Две функции с различными списками параметров, объявленные как extern "C", редактор связей воспринимает как один и тот же символ.
Упражнение 9.1: Зачем может понадобиться объявлять перегруженные функции?
Упражнение 9.2: Как нужно объявить перегруженные варианты функции error(), чтобы были корректны следующие вызовы:
int index; int upperBound; char selectVal; // ... error("Array out of bounds: ", index, upperBound); error("Division by zero"); error("Invalid selection", selectVal);
Упражнение 9.3: Объясните, к какому эффекту приводит второе объявление в каждом из приведенных примеров:
// (a) int calc(int, int); int calc(const int, const int); // (b) int get(); double get(); // (c) int *reset(int *); double *reset(double *): // (d) extern "C" int compute(int *, int); extern "C" double compute(double *, double);
Упражнение 9.4: Какая из следующих инициализаций приводит к ошибке? Почему?
// (a) void reset(int *); void (*pf)(void *) = reset; // (b) int calc(int, int); int (*pf1)(int, int) = calc; // (c) extern "C" int compute(int *, int); int (*pf3)(int*, int) = compute; // (d) void (*pf4)(const matrix&) = 0;
Разрешением перегрузки функции называется процесс выбора той функции из множества перегруженных, которую следует вызвать. Этот процесс основывается на указанных при вызове аргументах. Рассмотрим пример:
T t1, t2; void f(int, int); void f(float, float); int main() { f(t1, t2); return 0; }
Здесь в ходе процесса разрешения перегрузки в зависимости от типа T определяется, будет ли при обработке выражения f(t1,t2) вызвана функция f(int,int) или f(float,float) или зафиксируется ошибка.
Разрешение перегрузки функции – один и самых сложных аспектов языка C++. Пытаясь разобраться во всех деталях, начинающие программисты столкнутся с серьезными трудностями. Поэтому в данном разделе мы представим лишь краткий обзор того, как происходит разрешение перегрузки, чтобы у вас составилось хоть какое-то впечатление об этом процессе. Для тех, кто хочет узнать больше, в следующих двух разделах приводится более подробное описание.
Процесс разрешения перегрузки функции состоит из трех шагов, которые мы покажем на следующем примере:
void f(); void f(int); void f(double, double = 3.4); void f(char *, char *); int main() { f(5.6); return 0; }
При разрешении перегрузки функции выполняются следующие шаги:
Рассмотрим последовательно каждый пункт.
На первом шаге необходимо идентифицировать множество перегруженных функций, которые будут рассматриваться при данном вызове. Вошедшие в это множество функции называются кандидатами. Функция-кандидат – это функция с тем же именем, что и вызванная, причем ее объявление видимо в точке вызова. В нашем примере есть четыре таких кандидата: f(), f(int), f(double, double) и f(char*, char*).
После этого идентифицируются свойства списка переданных аргументов, т.е. их количество и типы. В нашем примере список состоит из двух аргументов типа double.
На втором шаге среди множества кандидатов отбираются устоявшие (viable) – такие, которые могут быть вызваны с данными аргументами, Устоявшая функция либо имеет столько же формальных параметров, сколько фактических аргументов передано вызванной функции, либо больше, но тогда для каждого дополнительного параметра должно быть задано значение по умолчанию. Чтобы функция считалась устоявшей, для любого фактического аргумента, переданного при вызове, обязано существовать преобразование к типу формального параметра, указанного в объявлении.
В нашем примере есть две устоявших функции, которые могут быть вызваны с приведенными аргументами:
Если после второго шага не нашлось устоявших функций, то вызов считается ошибочным. В таких случаях мы говорим, что имеет место отсутствие соответствия.
Третий шаг заключается в выборе функции, лучше всего отвечающей контексту вызова. Такая функция называется наилучшей из устоявших (или наиболее подходящей). На этом шаге производится ранжирование преобразований, использованных для приведения типов фактических аргументов к типам формальных параметров устоявшей функции. Наиболее подходящей считается функция, для которой выполняются следующие условия:
преобразования, примененные к фактическим аргументам, не хуже преобразований, необходимых для вызова любой другой устоявшей функции;
для некоторых аргументов примененные преобразования лучше, чем преобразования, необходимые для приведения тех же аргументов в вызове других устоявших функций.
Преобразования типов и их ранжирование более подробно обсуждаются в разделе 9.3. Здесь мы лишь кратко рассмотрим ранжирование преобразований для нашего примера. Для устоявшей функции f(int) должно быть применено приведение фактического аргумента типа double к типу int, относящееся к числу стандартных. Для устоявшей функции f(double,double) тип фактического аргумента double в точности соответствует типу формального параметра. Поскольку точное соответствие лучше стандартного преобразования (отсутствие преобразования всегда лучше, чем его наличие), то наиболее подходящей функцией для данного вызова считается f(double,double).
Если на третьем шаге не удается отыскать единственную лучшую из устоявших функцию, иными словами, нет такой устоявшей функции, которая подходила бы больше всех остальных, то вызов считается неоднозначным, т.е. ошибочным.
Процесс разрешения используется также при вызовах перегруженной функции-члена класса и перегруженного оператора. При разрешении перегрузки следует также принимать во внимание функции, конкретизированные из шаблонов.
Упражнение 9.5: Что происходит на последнем (третьем) шаге процесса разрешения перегрузки функции?
На втором шаге процесса разрешения перегрузки функции компилятор идентифицирует и ранжирует преобразования, которые следует применить к каждому фактическому аргументу вызванной функции для приведения его к типу соответствующего формального параметра любой из устоявших функций. Ранжирование может дать один из трех возможных результатов:
void print(unsigned int); void print(const char*); void print(char);
unsigned int a; print('a'); // соответствует print(char); print("a"); // соответствует print(const char*); print(a); // соответствует print(unsigned int);
void ff(char); ff(0); // аргумент типа int приводится к типу char
// функции print() объявлены так же, как и выше int *ip; class SmallInt { /* ... */ }; SmallInt si; print(ip); // ошибка: нет соответствия print(si); // ошибка: нет соответствия
Для установления точного соответствия тип фактического аргумента необязательно должен совпадать с типом формального параметра. К аргументу могут быть применены некоторые тривиальные преобразования, а именно:
(Подробнее они рассмотрены ниже.)Категория соответствия с преобразованием типа
является наиболее сложной. Необходимо рассмотреть несколько видов такого приведения:
расширение типов (promotions), стандартные преобразования и определенные пользователем
преобразования. (Расширения типов и стандартные преобразования изучаются в этой
главе. Определенные пользователем преобразования будут представлены позднее,
после детального рассмотрения классов; они выполняются конвертером, функцией-членом,
которая позволяет определить в классе собственный набор стандартных
трансформаций.
В главе 15 мы познакомимся с такими конвертерами и с тем, как они влияют на
разрешение перегрузки функций.)
При выборе лучшей из устоявших функций для данного вызова компилятор ищет функцию,
для которой применяемые к фактическим аргументам преобразования являются наилучшими
.
Преобразования типов ранжируются следующим образом: точное соответствие лучше
расширения типа, расширение типа лучше стандартного преобразования, а оно, в
свою очередь, лучше определенного пользователем преобразования. Мы еще вернемся
к ранжированию в разделе 9.4, а пока на простых примерах покажем, как оно помогает
выбрать наиболее подходящую функцию.
Самый простой случай возникает тогда, когда типы фактических аргументов совпадают с типами формальных параметров. Например, есть две показанные ниже перегруженные функции max(). Тогда каждый из вызовов max() точно соответствует одному из объявлений:
int max(int, int); double max(double, double); int i1; void calc(double d1) { max(56, i1); // точно соответствует max(int, int); max(d1, 66.9); // точно соответствует max(double, double); }
Перечислимый тип точно соответствует только определенным в нем элементам перечисления, а также объектам, которые объявлены как принадлежащие к этому типу:
enum Tokens { INLINE = 128; VIRTUAL = 129; }; Tokens curTok = INLINE; enum Stat { Fail, Pass }; extern void ff(Tokens); extern void ff(Stat); extern void ff(int); int main() { ff(Pass); // точно соответствует ff(Stat) ff(0); // точно соответствует ff(int) ff(curTok); // точно соответствует ff(Tokens) // ... }
Выше уже упоминалось, что фактический аргумент может точно соответствовать формальному параметру, даже если для приведения их типов необходимо некоторое тривиальное преобразование, первое из которых – преобразование l-значения в r-значение. Под l-значением понимается объект, удовлетворяющий следующим условиям:
Напротив, r-значение – это выражение, значение которого вычисляется, или выражение, обозначающее временный объект, для которого нельзя получить адрес и значение которого нельзя модифицировать. Вот простой пример:
int calc(int); int main() { int lval, res; lval = 5; // lvalue: lval; rvalue: 5 res = calc(lval); // lvalue: res // rvalue: временный объект для хранения значения, // возвращаемого функцией calc() return 0; }
В первом операторе присваивания переменная lval – это l-значение, а литерал 5 – r-значение. Во втором операторе присваивания res – это l-значение, а временный объект, в котором хранится результат, возвращаемый функцией calc(), – это r-значение.
В некоторых ситуациях в контексте, где ожидается значение, можно использовать выражение, представляющее собой l-значение:
int obj1; int obj2; int main() { // ... int local = obj1 + obj2; return 0; }
Здесь obj1 и obj2 – это l-значения. Однако для выполнения сложения в функции main() из переменных obj1 и obj2 извлекаются их значения. Действие, состоящее в извлечении значения объекта, представленного выражением вида l-значение, называется преобразованием l-значения в r-значение.
Когда функция ожидает аргумент, переданный по значению, то в случае, если аргумент является l-значением, выполняется его преобразование в r-значение:
#include <string> std::string color("purple"); void print(std::string); int main() { print(color); // точное соответствие: преобразование lvalue // в rvalue return 0; }
Так как аргумент в вызове print(color) передается по значению, то производится преобразование l-значения в r-значение для извлечения значения color и передачи его в функцию с прототипом print(string). Однако несмотря на то, что такое приведение имело место, считается, что фактический аргумент color точно соответствует объявлению print(string).
При вызове функций не всегда требуется применять к аргументам подобное преобразование. Ссылка представляет собой l-значение; если у функции есть параметр-ссылка, то при вызове функция получает l-значение. Поэтому к фактическому аргументу, которому соответствует формальный параметр-ссылка, описанное преобразование не применяется. Например, пусть объявлена такая функция:
#include <list> void print(std::list<int>&);
В вызове ниже li – это l-значение, представляющее объект std::list<int>, передаваемый функции print():
std::list<int> li(20); int main() { // ... print(li); // точное соответствие: нет преобразования lvalue в // rvalue return 0; }
Сопоставление li с параметром-ссылкой считается точным соответствием.
Второе преобразование, при котором все же фиксируется точное соответствие, –
это преобразование массива в указатель. Как уже отмечалось в разделе 7.3, параметр
функции никогда не имеет тип массива, трансформируясь вместо этого в указатель
на его первый элемент. Аналогично фактический аргумент типа массива из NT (где
N – число элементов в массиве, а T – тип каждого элемента) всегда приводится
к типу указателя на T. Такое преобразование типа фактического аргумента и называется
преобразованием массива в указатель. Несмотря на это, считается, что фактический
аргумент точно соответствует формальному параметру типа указатель на T
. Например:
int ai[3]; void putValues(int *); int main() { // ... putValues(ai); // точное соответствие: преобразование массива в // указатель return 0; }
Перед вызовом функции putValues() массив преобразуется в указатель, в результате чего фактический аргумент ai (массив из трех целых) приводится к указателю на int. Хотя формальным параметром функции putValues() является указатель и фактический аргумент при вызове преобразован, между ними устанавливается точное соответствие.
При установлении точного соответствия допустимо также преобразование функции в указатель.
Как и параметр-массив, параметр-функция становится указателем на функцию. Фактический
аргумент типа функция
также автоматически приводится к типу указателя на функцию.
Такое преобразование типа фактического аргумента и называется преобразованием
функции в указатель. Хотя трансформация производится, считается, что фактический
аргумент точно соответствует формальному параметру. Например:
int lexicoCompare(const string&, const string&); typedef int (*PFI)(const string&, const string&); void sort(string *, string *, PFI); string as[10]; int main() { // ... sort(as, as + sizeof(as)/sizeof(as[0] - 1), lexicoCompare // точное соответствие, преобразование функции в указатель ); return 0; }
Перед вызовом sort() применяется преобразование функции в указатель, которое
приводит аргумент lexicoCompare от типа функция
к типу указатель на функцию
.
Хотя формальным параметром функции является указатель, а фактическим – имя функции
и, следовательно, было произведено преобразование функции в указатель, считается,
что фактический аргумент точно третьему формальному параметру функции sort().
Последнее из перечисленных выше – это преобразование спецификаторов. Оно относится только к указателям и заключается в добавлении спецификаторов const или volatile (или обоих) к типу, который адресует данный указатель:
int a[5] = { 4454, 7864, 92, 421, 938 }; int *pi = a; bool is_equal(const int * , const int *); void func(int *parm) { // точное соответствие между pi и parm: преобразование спецификаторов if (is_equal(pi, parm)) // ... return 0; }
Перед вызовом функции is_equal() фактические аргументы pi и parm преобразуются
из типа указатель на int
в тип указатель на const int
. Эта трансформация
заключается в добавлении спецификатора const к адресуемому типу, поэтому относится
к категории преобразований спецификаторов. Несмотря на то, что функция ожидает
получить два указателя на const int, а фактические аргументы являются указателями
на int, считается, что точное соответствие между формальными и фактическими
параметрами функции is_equal() установлено.
Преобразование спецификаторов применимо только к типу, который адресует указатель. Оно не употребляется в случае, когда формальный параметр имеет спецификатор const или volatile, а фактический аргумент – нет.
extern void takeCI(const int); int main() { int ii = ...; takeCI(ii); // преобразование спецификаторов не применяется return 0; }
Хотя формальный параметр функции takeCI() имеет тип const int, а вызывается она с аргументом ii типа int, преобразование спецификаторов не производится: есть точное соответствие между фактическим аргументом и формальным параметром. Все сказанное верно и для случая, когда аргумент является указателем, а спецификаторы const или volatile относятся к этому указателю:
extern void init(int *const); extern int *pi; int main() { // ... init(pi); // преобразование спецификаторов не применяется return 0; }
Спецификатор const при формальном параметре функции init() относится к самому указателю, а не к типу, который он адресует. Поэтому компилятор при анализе преобразований, которые должны быть применены к фактическому аргументу, не учитывает этот спецификатор. К аргументу pi не применяется преобразование спецификатора: считается, что этот аргумент и формальный параметр точно соответствуют друг другу.
Первые три из рассмотренных преобразований (l-значения в r-значение, массива в указатель и функции в указатель) часто называют трансформациями l-значений. (В разделе 9.4 мы увидим, что хотя и трансформации l-значений, и преобразования спецификаторов относятся к категории преобразований, не нарушающих точного соответствия, его степень считается выше в случае, когда необходима лишь первая трансформация. В следующем разделе мы поговорим об этом несколько подробнее.)
Точное соответствие можно установить принудительно, воспользовавшись явным приведением типов. Например, если есть две перегруженные функции:
extern void ff(int); extern void ff(void *);
то вызов
ff(0xffbc); // вызывается ff(int)
будет точно соответствовать ff(int), хотя литерал 0xffbc записан в виде шестнадцатеричной константы. Программист может заставить компилятор вызвать функцию ff(void *), если явно выполнит операцию приведения типа:
ff(reinterpret_cast<void *>(0xffbc)); // вызывается ff(void*)
Если к фактическому аргументу применяется такое приведение, то он приобретает тип, в который преобразуется. Явные приведения типов помогают в управлении процессом разрешения перегрузки. Например, если при разрешении перегрузки получается неоднозначный результат (фактические аргументы одинаково хорошо соответствуют двум или более устоявшим функциям), то для устранения неоднозначности можно применить явное приведение типа, заставив компилятор выбрать конкретную функцию.
Под расширением типа понимается одно из следующих преобразований:
Подобное расширение применяется, когда тип фактического аргумента совпадает с одним из только что перечисленных типов, а формальный параметр относится к соответствующему расширенному типу:
extern void manip(int); int main() { manip('a'); // тип char расширяется до int return 0; }
Символьный литерал имеет тип char. Он расширяется до int. Поскольку расширенный тип соответствует типу формального параметра функции manip(), мы говорим, что ее вызов требует расширения типа аргумента.
Рассмотрим следующий пример:
extern void print(unsigned int); extern void print(int); extern void print(char); unsigned char uc; print(uc); // print(int); для uc требуется только расширение типа
Для аппаратной платформы, на которой unsigned char занимает один байт памяти, а int – четыре байта, расширение преобразует unsigned char в int, так как с его помощью можно представить все значения типа unsigned char. Для такой машинной архитектуры из приведенного в примере множества перегруженных функций наилучшее соответствие аргументу типа unsigned char обеспечивает print(int). Для двух других функций установление соответствия требует стандартного приведения.
Следующий пример иллюстрирует расширение фактического аргумента перечислимого типа:
enum Stat (Fail, Pass); extern void ff(int); extern void ff(char); int main() { // правильно: элемент перечисления Pass расширяется до типа int ff(Pass); // ff(int) ff(0); // ff(int) }
Иногда расширение перечислений преподносит сюрпризы. Компиляторы часто выбирают представление перечисления в зависимости от значений его элементов. Предположим, что в вышеупомянутой архитектуре (один байт для char и четыре байта для int) определено такое перечисление:
enum e1 { a1, b1, c1 };
Поскольку есть всего три элемента: a1, b1 и c1 со значениями 0, 1 и 2 соответственно – и поскольку все эти значения можно представить типом char, то компилятор, как правило, и выбирает char для представления типа e1. Рассмотрим, однако, перечисление e2 со следующим множеством элементов:
enum e2 { a2, b2, c2=0x80000000 };
Так как одна из констант имеет значение 0x80000000, то компилятор обязан выбрать для представления e2 такой тип, который достаточен для хранения значения 0x80000000, то есть unsigned int. Итак, хотя и e1, и e2 являются перечислениями, их представления различаются. Из-за этого e1 и e2 расширяются до разных типов:
#include <string> std::string format(int); std::string format(unsigned int); int main() { format(a1); // вызывается format(int) format(a2); // вызывается format(unsigned int) return 0; }
При первом обращении к format() фактический аргумент расширяется до типа int, так как для представления типа e1 используется char, и, следовательно, вызывается перегруженная функция format(int). При втором обращении тип фактического аргумента e2 представлен типом unsigned int и аргумент расширяется до unsigned int, из-за чего вызывается перегруженная функция format(unsigned int). Поэтому следует помнить, что поведение двух перечислений по отношению к процессу разрешения перегрузки может быть различным и зависеть от значений элементов, определяющих, как происходит расширение типа.
Имеется пять видов стандартных преобразований, а именно:
Вот несколько примеров:
extern void print(void*); extern void print(double); int main() { int i; print(i); // соответствует print(double); // i подвергается стандартному преобразованию из int в double print(&i); // соответствует print(void*); // &i подвергается стандартному преобразованию // из int* в void* return 0; }
Преобразования, относящиеся к группам 1, 2 и 3, потенциально опасны, так как целевой тип может и не обеспечивать представления всех значений исходного. Например, с помощью float нельзя адекватно представить все значения типа int. Именно по этой причине трансформации, входящие в эти группы, отнесены к категории стандартных преобразований, а не расширений типов.
int i; void calc(float); int main() { calc(i); // стандартное преобразование между целым типом и типом с // плавающей точкой потенциально опасно в зависимости от // значения i return 0; }
При вызове функции calc() применяется стандартное преобразование из целого типа int в тип с плавающей точкой float. В зависимости от значения переменной i может оказаться, что его нельзя сохранить в типе float без потери точности.
Предполагается, что все стандартные изменения требуют одного объема работы. Например, преобразование из char в unsigned char не более приоритетно, чем из char в double. Близость типов не принимается во внимание. Если две устоявших функции требуют для установления соответствия стандартной трансформации фактического аргумента, то вызов считается неоднозначным и помечается компилятором как ошибка. Например, если даны две перегруженные функции:
extern void manip(long); extern void manip(float);
то следующий вызов неоднозначен:
int main() { manip(3.14); // ошибка: неоднозначность // manip(float) не лучше, чем manip(int) return 0; }
Константа 3.14 имеет тип double. С помощью того или иного стандартного преобразования соответствие может быть установлено с любой из перегруженных функций. Поскольку есть две трансформации, приводящие к цели, вызов считается неоднозначным. Ни одно преобразование не имеет преимущества над другим. Программист может разрешить неоднозначность либо путем явного приведения типа:
manip (static_cast<long>(3.14)); // manip(long)
либо используя суффикс, обозначающий, что константа принадлежит к типу float:
manip (3.14F)); // manip(float)
Вот еще несколько примеров неоднозначных вызовов, которые помечаются как ошибки, поскольку соответствуют нескольким перегруженным функциям:
extern void farith(unsigned int); extern void farith(float); int main() { // каждый из последующих вызовов неоднозначен farith('a'); // аргумент имеет тип char farith(0); // аргумент имеет тип int farith(2uL); // аргумент имеет тип unsigned long farith(3.14159); // аргумент имеет тип double farith(true); // аргумент имеет тип bool }
Стандартные преобразования указателей иногда противоречат интуиции. В частности, значение 0 приводится к указателю на любой тип; полученный таким образом указатель называется нулевым. Значение 0 может быть представлено как константное выражение целого типа:
void set(int*); int main() { // преобразование указателя из 0 в int* применяется к аргументам // в обоих вызовах set(0L); set(0x00); return 0; }
Константное выражение 0L (значение 0 типа long int) и константное выражение 0x00 (шестнадцатеричное целое значение 0) имеют целый тип и потому могут быть преобразованы в нулевой указатель типа int*.
Но поскольку перечисления не относятся к целым типам, элемент, равный 0, не приводим к типу указателя:
enum EN { zr = 0 }; set(zr); // ошибка: zr нельзя преобразовать в тип int*
Вызов функции set() является ошибкой, так как не существует преобразования между значением zr элемента перечисления и формальным параметром типа int*, хотя zr равно 0.
Следует отметить, что константное выражение 0 имеет тип int. Для его приведения к типу указателя требуется стандартное преобразование. Если в множестве перегруженных функций есть функция с формальным параметром типа int, то именно в ее пользу будет разрешена перегрузка в случае, когда фактический аргумент равен 0:
void print(int); void print(void *); void set(const char *); void set(char *); int main () { print(0); // вызывается print(int); set(0); // неоднозначность return 0; }
При вызове print(int) имеет место точное соответствие, тогда как для вызова print(void*) необходимо приведение значения 0 к типу указателя. Поскольку соответствие лучше преобразования, для разрешения этого вызова выбирается функция print(int). Обращение к set() неоднозначно, так как 0 соответствует формальным параметрам обеих перегруженных функций за счет применения стандартной трансформации. Раз обе функции одинаково хороши, фиксируется неоднозначность. Последнее из возможных преобразований указателя позволяет привести указатель любого типа к типу void*, поскольку void* – это родовой указатель на любой тип данных. Вот несколько примеров:
#include <string> extern void reset(void *); void func(int *pi, string *ps) { // ... reset(pi); // преобразование указателя: int* в void* /// ... reset(ps); // преобразование указателя: string* в void* }
Только указатели на типы данных могут быть приведены к типу void* с помощью стандартного преобразования, с указателями на функции так поступать нельзя:
typedef int (*PFV)(); extern PFV testCases[10]; // массив указателей на функции
extern void reset(void *); int main() { // ... reset(textCases[0]); // ошибка: нет стандартного преобразования // между int(*)() и void* return 0; }
Фактический аргумент или формальный параметр функции могут быть ссылками. Как это влияет на правила преобразования типов?
Рассмотрим, что происходит, когда ссылкой является фактический аргумент. Его тип никогда не бывает ссылочным. Аргумент-ссылка трактуется как l-значение, тип которого совпадает с типом соответствующего объекта:
int i; int& ri = i; void print(int); int main() { print(i); // аргумент - это lvalue типа int print(ri); // то же самое return 0; }
Фактический аргумент в обоих вызовах имеет тип int. Использование ссылки для его передачи во втором вызове не влияет на сам тип аргумента.
Стандартные преобразования и расширения типов, рассматриваемые компилятором, одинаковы для случаев, когда фактический аргумент является ссылкой на тип T и когда он сам имеет такой тип. Например:
int i; int& ri = i; void calc(double); int main() { calc(i); // стандартное преобразование между целым типом // и типом с плавающей точкой calc(ri); // то же самое return 0; }
А как влияет на преобразования, применяемые к фактическому аргументу, формальный параметр-ссылка? Сопоставление дает следующие результаты:
void swap(int&, int&); void manip(int i1, int i2) { // ... swap(i1, i2); // правильно: вызывается swap(int&, int &) // ... return 0; }
int obj; void frd(double &); int main() { frd(obj); // ошибка: параметр должен иметь иметь тип const double& return 0; }
class B; void takeB(B&); B giveB(); int main() { takeB(giveB()); // ошибка: параметр должен быть типа const B&; return 0; }
Следует отметить, что и преобразование l-значения в r-значение, и инициализация ссылки считаются точными соответствиями. В данном примере первый вызов функции приводит к ошибке:
void print(int); void print(int&); int iobj; int &ri = iobj; int main() { print(iobj); // ошибка: неоднозначность print(ri); // ошибка: неоднозначность print(86); // правильно: вызывается print(int) return 0; }
Объект iobj – это аргумент, для которого может быть установлено соответствие с обеими функциями print(), то есть вызов неоднозначен. То же относится и к следующей строке, где ссылка ri обозначает объект, соответствующий обеим функциям print(). С третьим вызовом, однако, все в порядке. Для него print(int&) не является устоявшей. Целая константа – это r-значение, так что она не может инициализировать параметр-ссылку. Единственной устоявшей функцией для вызова print(86) является print(int), поэтому она и выбирается при разрешении перегрузки.
Короче говоря, если формальный параметр представляет собой ссылку, то для фактического аргумента точное соответствие устанавливается, если он может инициализировать ссылку, и не устанавливается в противном случае.
Упражнение 9.6: Назовите два тривиальных преобразования, допустимых при установлении точного соответствия.
Упражнение 9.7: Каков ранг каждого из преобразований аргументов в следующих вызовах функций:
// (a) void print(int *, int); int arr[6]; print(arr, 6); // вызов функции // (b) void manip(int, int); manip('a', 'z'); // вызов функции // (c) int calc(int, int); double dobj; double = calc(55.4, dobj) // вызов функции // (d) void set(const int *); int *pi; set(pi); // вызов функции
Упражнение 9.8: Какие из данных вызовов ошибочны из-за того, что не существует преобразования между типом фактического аргумента и формального параметра:
// (a) enum Stat { Fail, Pass }; void test(Stat); text(0); // вызов функции // (b) void reset(void *); reset(0); // вызов функции // (c) void set(void *); int *pi; set(pi); // вызов функции // (d) #include <list> std::list<int> oper(); void print(oper()); // вызов функции // (e) void print(const int); int iobj; print(iobj); // вызов функции
В разделе 9.2 мы уже упоминали, что процесс разрешения перегрузки функций состоит из трех шагов:
Теперь мы готовы к тому, чтобы изучить эти шаги более детально.
Функцией-кандидатом называется функция, имеющая то же имя, что и вызванная. Кандидаты отыскиваются двумя способами:
void f(); void f(int); void f(double, double = 3.4); void f(char*, char*); int main() { f(5.6); // для разрешения этого вызова есть четыре кандидата return 0; }
namespace NS { class C { /* ... */ }; void takeC(C&); } // тип cobj - это класс C, объявленный в пространстве имен NS NS::C obj; int main() { // в точке вызова не видна ни одна из функций takeC() takeC(cobj); // правильно: вызывается NS::takeC(C&), // потому что аргумент имеет тип NS::C, следовательно, // принимается во внимание функция takeC(), // объявленная в пространстве имен NS return 0; }
Таким образом, совокупность кандидатов является объединением множества функций, видимых в точке вызова, и множества функций, объявленных в том же пространстве имен, к которому принадлежат типы фактических аргументов.
При идентификации множества перегруженных функций, видимых в точке вызова, применимы уже рассмотренные ранее правила.
Функция, объявленная во вложенной области видимости, скрывает, а не перегружает одноименную функцию во внешней области. В такой ситуации кандидатами будут только функции из во вложенной области, т.е. такие, которые не скрыты при вызове. В следующем примере функциями-кандидатами, видимыми в точке вызова, являются format(double) и format(char*):
char* format(int); void g() { char *format(double); char* format(char*); format(3); // вызывается format(double) }
Так как format(int), объявленная в глобальной области видимости, скрыта, она не включается в множество функций-кандидатов.
Кандидаты могут быть введены с помощью using-объявлений, видимых в точке вызова:
namespace libs_R_us { int max(int, int); double max(double, double); } char max(char, char); void func() { // функции из пространства имен невидимы // все три вызова разрешаются в пользу глобальной функции max(char, char) max(87, 65); max(35.5, 76.6); max('J', 'L'); }
Функции max(), определенные в пространстве имен libs_R_us, невидимы в точке вызова. Единственной видимой является функция max() из глобальной области; только она входит в множество функций-кандидатов и вызывается при каждом из трех обращений к func(). Мы можем воспользоваться using-объявлением, чтобы сделать видимыми функции max() из пространства имен libs_R_us. Куда поместить using-объявление? Если включить его в глобальную область видимости:
char max(char, char); using libs_R_us::max; // using-объявление
то функции max() из libs_R_us добавляются в множество перегруженных функций, которое уже содержит max(), объявленную в глобальной области. Теперь все три функции видны внутри func() и становятся кандидатами. В этой ситуации вызовы func() разрешаются следующим образом:
void func() { max(87, 65); // вызывается libs_R_us::max(int, int) max(35.5, 76.6); // вызывается libs_R_us::max(double, double) max('J', 'L'); // вызывается ::max(char, char) }
Но что будет, если мы введем using-объявление в локальную область видимости функции func(), как показано в данном примере?
void func() { // using-объявление using libs_R_us::max; // те же вызовы функций, что и выше }
Какие из функций max() будут включены в множество кандидатов? Напомним, что using-объявления вкладываются друг в друга. При наличии такого объявления в локальной области глобальная функция max(char, char) оказывается скрытой, так что в точке вызова видны только
libs_R_us::max(int, int); libs_R_us::max(double, double);
Они и являются кандидатами. Теперь вызовы func() разрешаются следующим образом:
void func() { // using-объявление // глобальная функция max(char, char) скрыта using libs_R_us::max; max(87, 65); // вызывается libs_R_us::max(int, int) max(35.5, 76.6); // вызывается libs_R_us::max(double, double) max('J', 'L'); // вызывается libs_R_us::max(int, int) }
Using-директивы также оказывают влияние на состав множества функций-кандидатов. Предположим, мы решили их использовать, чтобы сделать функции max() из пространства имен libs_R_us видимыми в func(). Если разместить следующую using-директиву в глобальной области видимости, то множество функций-кандидатов будет состоять из глобальной функции max(char, char) и функций max(int, int) и max(double, double), объявленных в libs_R_us:
namespace libs_R_us { int max(int, int); double max(double, double); } char max(char, char); using namespace libs_R_us; // using-директива void func() { max(87, 65); // вызывается libs_R_us::max(int, int) max(35.5, 76.6); // вызывается libs_R_us::max(double, double) max('J', 'L'); // вызывается ::max(int, int) }
Что будет, если поместить using-директиву в локальную область видимости, как в следующем примере?
void func() { // using-директива using namespace libs_R_us; // те же вызовы функций, что и выше }
Какие из функций max() окажутся среди кандидатов? Напомним, что using-директива делает члены пространства имен видимыми, словно они были объявлены вне этого пространства, в той точке, где такая директива помещается. В нашем примере члены libs_R_us видимы в локальной области функции func(), как будто они объявлены вне пространства – в глобальной области. Отсюда следует, что множество перегруженных функций, видимых внутри func(), то же, что и раньше, т.е. включает в себя
max(char, char); libs_R_us::max(int, int); libs_R_us::max(double, double);
В локальной или глобальной области видимости появляется using-директива, на разрешение вызовов функции func() не влияет:
void func() { using namespace libs_R_us; max(87, 65); // вызывается libs_R_us::max(int, int) max(35.5, 76.6); // вызывается libs_R_us::max(double, double) max('J', 'L'); // вызывается ::max(int, int) }
Итак, множество кандидатов состоит из функций, видимых в точке вызова, включая и те, которые введены using-объявлениями и using-директивами, а также из функций, объявленных в пространствах имен, ассоциированных с типами фактических аргументов. Например:
namespace basicLib { int print(int); double print(double); } namespace matrixLib { class matrix { /* ... */ }; void print(const maxtrix&); } void display() { using basicLib::print; matrixLib::matrix mObj; print(mObj); // вызывается maxtrixLib::print(const maxtrix&) print(87); // вызывается basicLib::print(const maxtrix&) }
Кандидатами для print(mObj) являются введенные using-объявлением внутри display() функции basicLib::print(int) и basicLib::print(double), поскольку они видимы в точке вызова. Так как фактический аргумент функции имеет тип matrixLib::matrix, то функция print(), объявленная в пространстве имен matrixLib, также будет кандидатом. Каковы функции-кандидаты для print(87)? Только basicLib::print(int) и basicLib::print(double), видимые в точке вызова. Поскольку аргумент имеет тип int, дополнительное пространство имен в поисках других кандидатов не рассматривается.
Устоявшая функция относится к числу кандидатов. В списке ее формальных параметров либо то же самое число элементов, что и в списке фактических аргументов вызванной функции, либо больше. В последнем случае для дополнительных параметров задаются значения по умолчанию, иначе функцию нельзя будет вызвать с данным числом аргументов. Чтобы функция считалась устоявшей, должно существовать преобразование каждого фактического аргумента в тип соответствующего формального параметра. (Такие преобразования были рассмотрены в разделе 9.3.) В следующем примере для вызова f(5.6) есть две устоявшие функции: f(int) и f(double).
void f(); void f(int); void f(double); void f(char*, char*); int main() { f(5.6); // 2 устоявшие функции: f(int) и f(double) return 0; }
Функция f(int) устояла, так как она имеет всего один формальный параметр, что соответствует числу фактических аргументов в вызове. Кроме того, существует стандартное преобразование аргумента типа double в int. Функция f(double) также устояла; она тоже имеет один параметр типа double, и он точно соответствует фактическому аргументу. Функции-кандидаты f() и f(char*, char*) исключены из списка устоявших, так как они не могут быть вызваны с одним аргументом. В следующем примере единственной устоявшей функцией для вызова format(3) является format(double). Хотя кандидата format(char*) можно вызывать с одним аргументом, не существует преобразования из типа фактического аргумента int в тип формального параметра char*, а следовательно, функция не может считаться устоявшей.
char* format(int); void g() { // глобальная функция format(int) скрыта char* format(double); char* format(char*); format(3); // есть только одна устоявшая функция: format(double) }
В следующем примере все три функции-кандидата оказываются устоявшими для вызова max() внутри func(). Все они могут быть вызваны с двумя аргументами. Поскольку фактические аргументы имеют тип int, они точно соответствуют формальным параметрам функции libs_R_us::max(int, int) и могут быть приведены к типам параметров функции libs_R_us::max(double, double) с помощью трансформации целых в плавающие, а также к типам параметров функции libs_R_us::max(char, char) посредством преобразования целых типов.
namespace libs_R_us { int max(int, int); double max(double, double); } // using-объявление using libs_R_us::max; char max(char, char); void func() { // все три функции max() являются устоявшими max(87, 65); // вызывается using libs_R_us::max(int, int) }
Обратите внимание, что функция-кандидат с несколькими параметрами исключается из числа устоявших, как только выясняется, что один из фактических аргументов не может быть приведен к типу соответствующего формального параметра, пусть даже для всех остальных аргументов такое преобразование существует. В следующем примере функция min(char *, int) исключается из множества устоявших, поскольку нет возможности трансформации типа первого аргумента int в тип соответствующего параметра char *. И это происходит несмотря на то, что второй аргумент точно соответствует второму параметру.
extern double min(double, double); extern double min(char*, int); void func() { // одна функция-кандидат min(double, double) min(87, 65); // вызывается min(double, double) }
Если после исключения из множества кандидатов всех функций с несоответствующим числом параметров и тех, для параметров которых не оказалось подходящего преобразования, не осталось устоявших, то обработка вызова функции заканчивается ошибкой компиляции. В таком случае говорят, что соответствия не найдено.
void print(unsigned int); void print(char*); void print(char); int *ip; class SmallInt { /* ... */ }; SmallInt si; int main() { print(ip); // ошибка: нет устоявших функций: соответствие не найдено print(si); // ошибка: нет устоявших функций: соответствие не найдено return 0; }
Наилучшей считается та из устоявших функций, формальные параметры которой наиболее точно соответствуют типам фактических аргументов. Для любой такой функции преобразования типов, применяемые к каждому аргументу, ранжируются для определения степени его соответствия параметру. Наилучшей из устоявших называют функцию, для которой одновременно выполняются два условия:
Может оказаться так, что для приведения фактического аргумента к типу соответствующего формального параметра нужно выполнить несколько преобразований. Так, в следующем примере
int arr[3]; void putValues(const int *); int main() { putValues(arr); // необходимо 2 преобразования // массив в указатель + преобразование спецификатора return 0; }
для приведения аргумента arr от типа массив из трех int
к типу указатель на const int
применяется последовательность преобразований:
Поэтому было бы более правильно говорить, что для приведения фактического аргумента к типу формального параметра устоявшей функции требуется последовательность преобразований. Поскольку применяется не одна, а несколько трансформаций, то на третьем шаге процесса разрешения перегрузки функции на самом деле ранжируются последовательности преобразований.
Рангом такой последовательности считается ранг самой плохой из входящих в нее трансформаций. Как объяснялось в разделе 9.2, преобразования типов ранжируются следующим образом: точное соответствие лучше расширения типа, а расширение типа лучше стандартного преобразования. В предыдущем примере оба изменения имеют ранг точного соответствия. Поэтому и у всей последовательности такой же ранг.
Такая совокупность состоит из нескольких преобразований, применяемых в указанном порядке:
//преобразование l-значения -> //расширение типа или стандартное преобразование -> //преобразование спецификаторов
Термин преобразование l-значения относится к первым трем трансформациям из категории точных соответствий, рассмотренных в разделе 9.2: преобразование l-значения в r-значение, преобразование массива в указатель и преобразование функции в указатель. Последовательность трансформаций состоит из нуля или одного преобразования l-значения, за которым следует нуль или одно расширение типа или стандартное преобразование, и наконец нуль или одно преобразование спецификаторов. Для приведения фактического аргумента к типу формального параметра может быть применено только одна трансформация каждого вида.
Описанная последовательность называется последовательностью стандартных преобразований. Существует также последовательность определенных пользователем преобразований, которая связана с функцией-конвертером, являющейся членом класса. (Конвертеры и последовательности определенных пользователем преобразований рассматриваются в главе 15.)
Каковы последовательности изменений фактических аргументов в следующем примере?
namespace libs_R_us { int max(int, int); double max(double, double); } // using-объявление using libs_R_us::max; void func() { char c1, c2; max(c1, c2); // вызывается libs_R_us::max(int, int) }
Аргументы в вызове функции max() имеют тип char. Последовательность преобразований аргументов при вызове функции libs_R_us::max(int,int) следующая:
1a. Так как аргументы передаются по значению, то с помощью преобразования l-значения в r-значение извлекаются значения аргументов c1 и c2.
2a. С помощью расширения типа аргументы трансформируются из char в int.
Последовательность преобразований аргументов при вызове функции libs_R_us::max(double,double) следующая:
1b. С помощью преобразования l-значения в r-значение извлекаются значения аргументов c1 и c2.
2b. Стандартное преобразование между целым и плавающим типом приводит аргументы от типа char к типу double.
Ранг первой последовательности – расширение типа (самое худшее из примененных изменений), тогда как ранг второй – стандартное преобразование. Так как расширение типа лучше, чем преобразование, то в качестве наилучшей из устоявших для данного вызова выбирается функция libs_R_us::max(int,int).
Если ранжирование последовательностей преобразований аргументов не может выявить единственной устоявшей функции, то вызов считается неоднозначным. В данном примере для обоих вызовов calc() требуется такая последовательность:
Поскольку нельзя сказать, какая из этих последовательностей лучше другой, вызов неоднозначен:
int i, j; extern long calc(long, long); extern double calc(double, double); void jj() { // ошибка: неоднозначность, нет наилучшего соответствия calc(i, j); }
Преобразование спецификаторов (добавление спецификатора const или volatile к типу, который адресует указатель) имеет ранг точного соответствия. Однако, если две последовательности трансформаций отличаются только тем, что в конце одной из них есть дополнительное преобразование спецификаторов, то последовательность без него считается лучше. Например:
void reset(int *); void reset(const int *); int* pi; int main() { reset(pi); // без преобразования спецификаторов лучше: // выбирается reset(int *) return 0; }
Последовательность стандартных преобразований, примененная к фактическому аргументу
для первой функции-кандидата reset(int*), – это точное соответствие, требуется
лишь переход от l-значения к r-значению, чтобы извлечь значение аргумента. Для
второй функции-кандидата reset(const int *) также применяется трансформация
l-значения в r-значение, но за ней следует еще и преобразование спецификаторов
для приведения результирующего значения от типа указатель на int
к типу указатель на const int
.
Обе последовательности представляют собой точное соответствие,
но неоднозначности при этом не возникает. Так как вторая последовательность
отличается от первой наличием трансформации спецификаторов в конце, то последовательность
без такого преобразования считается лучшей. Поэтому наилучшей из устоявших функций
будет reset(int*).
Вот еще пример, в котором приведение спецификаторов влияет на то, какая последовательность будет выбрана:
int extract(void *); int extract(const void *); int* pi; int main() { extract(pi); // выбирается extract(void *) return 0; }Здесь для вызова есть две устоявших функции: extract(void*) и extract(const void*). Последовательность преобразований для функции extract(void*) состоит из трансформации l-значения в r-значение для извлечения значения аргумента, сопровождаемого стандартным преобразованием указателя: из указателя на int в указатель на void. Для функции extract(const void*) такая последовательность отличается от первой дополнительным преобразованием спецификаторов для приведения типа результата от указателя на void к указателю на const void. Поскольку последовательности различаются лишь этой трансформацией, то первая выбирается как более подходящая и, следовательно, наилучшей из устоявших будет функция extract(const void*). Спецификаторы const и volatile влияют также на ранжирование инициализации параметров-ссылок. Если две такие инициализации отличаются только добавлением спецификатора const и volatile, то инициализация без дополнительной спецификации считается лучшей при разрешении перегрузки:
#include <vector> void manip(std::vector<int>&); void manip(const std::vector<int>&); std::vector<int> f(); extern std::vector<int> vec; int main() { manip(vec); // выбирается manip(std::vector<int> &) manip(f()); // выбирается manip(const std::vector<int>&) return 0; }
В первом вызове инициализация ссылок для вызова любой функции является точным соответствием. Но этот вызов все же не будет неоднозначным. Так как обе инициализации одинаковы во всем, кроме наличия дополнительной спецификации const во втором случае, то инициализация без такой спецификации считается лучше, поэтому перегрузка будет разрешена в пользу устоявшей функции manip(std::vector<int>&).
Для второго вызова существует только одна устоявшая функция manip(const std::vector<int>&). Поскольку фактический аргумент является временной переменной, содержащей результат, возвращенный f(), то такой аргумент представляет собой r-значение, которое нельзя использовать для инициализации неконстантного формального параметра-ссылки функции manip(std::vector<int>&). Поэтому наилучшей является единственная устоявшая manip(const std::vector<int>&).
Разумеется, у функций может быть несколько фактических аргументов. Выбор наилучшей из устоявших должен производиться с учетом ранжирования последовательностей преобразований всех аргументов. Рассмотрим пример:
extern int ff(char*, int); extern int ff(int, int); int main() { ff(0, 'a'); // ff(int, int) return 0; }
Функция ff(), принимающая два аргумента типа int, выбирается в качестве наилучшей из устоявших по следующим причинам:
Вот еще один пример:
int compute(const int&, short); int compute(int&, double); extern int iobj; int main() { compute(iobj, 'c'); // compute(int&, double) return 0; }
Обе функции compute(const int&, short) и compute(int&, double) устояли. Вторая выбирается в качестве наилучшей по следующим причинам:
Наличие аргументов со значениями по умолчанию способно расширить множество устоявших функций. Устоявшими являются функции, которые вызываются с данным списком фактических аргументов. Но такая функция может иметь больше формальных параметров, чем задано фактических аргументов, в том случае, когда для каждого неуказанного параметра есть некое значение по умолчанию:
extern void ff(int); extern void ff(long, int = 0); int main() { ff(2L); // соответствует ff(long, 0); ff(0, 0); // соответствует ff(long, int); ff(0); // соответствует ff(int); ff(3.14); // ошибка: неоднозначность }
Для первого и третьего вызовов функция ff() является устоявшей, хотя передан всего один фактический аргумент. Это обусловлено следующими причинами:
Последний вызов является неоднозначным, поскольку обе устоявших функции могут быть выбраны, если применить стандартное преобразование к первому аргументу.
Функции ff(int) не отдается предпочтение только потому, что у нее один параметр.
Упражнение 9.9: Объясните, что происходит при разрешении перегрузки для вызова функции compute() внутри main(). Какие функции являются кандидатами? Какие из них устоят после первого шага? Какие последовательности преобразований надо применить к фактическому аргументу, чтобы он соответствовал формальному параметру для каждой устоявшей функции? Какая функция будет наилучшей из устоявших?
namespace primerLib { void compute(); void compute(const void *); } using primerLib::compute; void compute(int); void compute(double, double = 3.4); void compute(char*, char* = 0); int main() { compute(0); return 0; }
Что будет, если using-объявление поместить внутрь main() перед вызовом compute()? Ответьте на те же вопросы.