Перегрузка функций. Подставляемые (встраиваемые) функции
Аннотация: В лекции рассматриваются понятия, объявление и использование в программах подставляемых и перегруженных функций в С++, механизмы выполнения подстановки и перегрузки функций, рекомендации по повышению эффективности программ за счет перегрузки или подстановки функций.
Цель лекции : изучить подставляемые (встраиваемые) функции и перегрузки функций, научиться разрабатывать программы с использованием перегрузки функций на языке C++.
Подставляемые функции
Вызов функции , передача в нее значений, возврат значения – эти операции занимают довольно много процессорного времени. Обычно при определении функции компилятор резервирует в памяти только один блок ячеек для сохранения ее операторов. После вызова функции управление программой передается этим операторам, а по возвращении из функции выполнение программы возобновляется со строки, следующей после вызова функции.
При неоднократных вызовах каждый раз программа будет отрабатывать один и тот же набор команд, не создавая копий для каждого вызова в отдельности.
Каждый переход к области памяти, содержащей операторы функции, замедляет выполнение программы. Если функция занимает небольшой объем, то можно получить выигрыш во времени при многократных вызовах, дав компилятору команду встроить код функции непосредственно в программу по месту вызова. Такие функции называется подставляемыми . В этом случае, говоря об эффективности, прежде всего, подразумевается скорость выполнения программы.
Подставляемые или встраиваемые (inline) функции – это функции, код которых вставляется компилятором непосредственно на место вызова, вместо передачи управления единственному экземпляру функции.
Если функция является подставляемой, компилятор не создает данную функцию в памяти, а копирует ее строки непосредственно в код программы по месту вызова. Это равносильно вписыванию в программе соответствующих блоков вместо вызовов функций. Таким образом, спецификатор inline определяет для функции так называемое внутреннее связывание , которое заключается в том, что компилятор вместо вызова функции подставляет команды ее кода. Подставляемые функции используют, если тело функции состоит из нескольких операторов.
Этот подход позволяет увеличить скорость выполнения программы, так как из программы исключаются команды микропроцессора , требующиеся для передачи аргументов и вызова функции.
Например:
/*функция возвращает расстояние от точки с координатами(x1,y1) до точки с координатами (x2,y2)*/ inline float Line(float x1,float y1,float x2, float y2) { return sqrt(pow(x1-x2,2)+pow(y1-y2,2)); }
Однако следует обратить внимание, что использование подставляемых функций не всегда приводит к положительному эффекту. Если такая функция вызывается в программном коде несколько раз, то во время компиляции в программу будет вставлено столько же копий этой функции, сколько ее вызовов. Произойдет значительное увеличение размера программного кода, в результате чего ожидаемого повышения эффективности выполнения программы по времени может и не произойти.
Пример 1 .
#include "stdafx.h"
#include Перечислим причины, по которым функция
со спецификатором inline
будет трактоваться как обычная не подставляемая функция
: Ограничения на выполнение подстановки в основном зависят от реализации. Если же для функции со спецификатором inline
компилятор
не может выполнить подстановку из-за контекста, в который помещено обращение к ней, то функция
считается статической и выдается предупреждающее сообщение
. Еще одной из особенностей подставляемых функций является невозможность их изменения без перекомпиляции всех частей программы, в которых эти функции вызываются. При определении функций в программах необходимо указывать тип возвращаемого функцией значения, а также количество параметров и тип каждого из них. Если на языке С++ была написана функция
с именем add_values
, которая работала с двумя целыми значениями, а в программе было необходимо использовать подобную функцию для передачи трех целых значений, то тогда следовало бы создать функцию с другим именем. Например, add_two_values
и add_three_values
. Аналогично, если необходимо использовать подобную функцию для работы со значениями типа float
, то нужна еще одна функция
с еще одним именем. Чтобы избежать дублирования функции, C++ позволяет определять несколько функций с одним и тем же именем. В процессе компиляции C++ принимает во внимание количество аргументов, используемых каждой функцией, и затем вызывает именно требуемую функцию. Предоставление компилятору выбора среди нескольких функций называется перегрузкой
. Перегрузка функций
– это создание нескольких функций с одним именем, но с разными параметрами. Под разными параметрами понимают, что должно быть разным количество аргументов
функции и/или их тип
. То есть перегрузка
функций позволяет определять несколько функций с одним и тем же именем и типом возвращаемого значения. Перегрузка
функций также называется полиморфизмом функций
. "Поли" означает много, "морфе" – форма, то есть полиморфическая функция
– это функция
, отличающаяся многообразием форм. Под полиморфизмом
функции понимают существование в программе нескольких перегруженных версий функции, имеющих разные значения. Изменяя количество или тип параметров, можно присвоить двум или нескольким функциям одно и тоже имя. При этом никакой путаницы не будет, поскольку нужная функция
определяется по совпадению используемых параметров. Это позволяет создавать функцию, которая сможет работать с целочисленными, вещественными значениями или значениями других типов без необходимости создавать отдельные имена для каждой функции. Таким образом, благодаря использованию перегруженных функций
, не следует беспокоиться о вызове в программе нужной функции, отвечающей типу передаваемых переменных. При вызове перегруженной функции компилятор
автоматически определит, какой именно вариант функции следует использовать. Например, следующая программа
перегружает функцию с именем add_values
. Первое определение
функции складывает два значения типа int
. Второе определение
функции складывает три значения типа int
. В процессе компиляции C++ корректно определяет функцию, которую необходимо использовать: #include "stdafx.h"
#include Таким образом, программа
определяет две функции с именами add_values
. Первая функция
складывает два значения, в то время как вторая складывает три значения одного типа int
. Компилятор
языка С++ определяет, какую функцию следует использовать, основываясь на предлагаемых программой параметрах. Одним из наиболее общих случаев использования перегрузки
является применение функции для получения определенного результата, исходя из различных параметров. Например, предположим, что в программе есть функция с именем day_of_week
, которая возвращает текущий день недели (0 для воскресенья, 1 для понедельника, ... , 6 для субботы). Программа могла бы перегрузить эту функцию таким образом, чтобы она верно возвращала день недели, если ей передан юлианский день в качестве параметра, или если ей переданы день, месяц и год. int day_of_week(int julian_day) {
// операторы
}
int day_of_week(int month, int day, int year) {
// операторы
} При использовании перегруженных функций
часто допускается ряд ошибок. Например, если функции отличаются только типом возвращаемого значения, но не типами аргументов, такие функции не могут иметь одинаковое имя. Также недопустим следующий вариант перегрузки
: int имя_функции(int имя_аргумента);
int имя_функции(int имя_аргумента);
/*недопустимая перегрузка имени: аргументы имеют одинаковое количество и одинаковый тип*/ Пример 2
. /*Перегруженные функции имеют одинаковые имена, но разные списки параметров и возвращаемые значения*/
#include "stdafx.h"
#include В языке С++
реализована возможность использования
одного идентификатора для функций,
выполняющих различные действия над
различными типами данных, в результате
чего можно использовать несколько
функций с одним и тем же именем, но с
разными списками параметров, как по
количеству, так и по типу. Такие функции
называют перегруженными
, а сам
механизм –перегрузка
функций
. Компилятор
определяет, к какой из функций с одним
и тем же именем следует обратиться
путем сравнения типов фактических
аргументов с типами формальных параметров
в заголовках всех этих функций, т.е.
компилятор в зависимости от типа и
количества аргументов будет формировать
необходимое обращение к соответствующей
функции. Поиск функции,
которую надо вызвать, осуществляется
за три отдельных шага: 1. Поиск функции
с точным соответствием параметров и
ее использование, если она найдена. 2. Поиск соответствующей
функции, используя встроенные
преобразования типов данных. 3. Поиск соответствующей
функции, используя преобразования,
определенные пользователем. Приведем пример
функции S
1 с двумя
параметрамих
,у
, работающая в
зависимости от типа передаваемых
аргументов, следующим образом: – если тип
параметров целочисленный, функция S
1
складывает их значения и возвращает
полученную сумму; – если тип
параметров long
, функцияS
1 перемножает их
значения и возвращает полученное
произведение; – если тип
параметров вещественный, функция S
1
делит их значения и возвращает частное
от деления. #
include int
S1 (int x, int y) { long
S1 (long x, long y) { double
S1 (double x, double y) { int
a = 1, b = 2, c; long
i = 3, j = 4, k; double
x = 10, y = 2, z; printf("\n
c = %d; k = %ld; z = %lf . \n", c, k, z); В результате
получим: c
= 3;
k
= 12;
z
= 5.000000 .
Многоточие в
списке параметров пользовательской
функции используется тогда, когда число
аргументов заранее неизвестно. При
этом неопределенное количество
параметров можно указать в ее прототипе
следующим образом: void
f1
(int
a
,
double
b
, …);
Такая запись
указывает компилятору на то, что за
обязательными фактическими аргументами
для параметров a
иb
могут следовать, а могут и не следовать
другие аргументы при вызове этой
функции. Перечислим
основные особенности использования
данного механизма. 1. Используется
несколько макрокоманд для доступа к
параметрам таких функций, это: va
_
list
иva
_
start
– макрокоманды подготовки доступа к
параметрам; va
_
arg
– использование параметров; va
_
end
– отчистка перед выходом. Они
объявлены в заголовочном файле stdarg
.
h
. 2. Такая функция
должна иметь минимум один параметр
(именованный) для передачи ей количества
передаваемых аргументов. 3. Для макроса
va
_
start
необходимо передать два аргумента –
имя списка параметров, который задаетva
_
list
и их количество. 4. Нарушать указанный
порядок макрокоманд нельзя. Иначе можно
получить непредсказуемые последствия. 5. Для макроса
va
_
arg
нужно помимо имени списка параметров
передать и предполагаемый тип. При
несоответствии типов – ошибка. Использование
многоточий полностью выключает проверку
типов параметров. Многоточие необходимо,
только если изменяются и число
параметров, и их тип. Следующий пример
иллюстрирует эту возможность. #include
#include
void
f1(double s, int n ...) { va_start(p,
n); printf("
\n Double S = %lf ", s); for(int
i=1; i<=n; i++) { v
= va_arg(p, int); printf("\n
Argument %d = %d ", i, v); void
main(void) { f1(1.5,
3, 4, 5, 6); В результате
получим: Double
S
= 1.500000
Argument
1 = 4
Argument 2 = 5
Argument 3 = 6
Press
any key to continue
C ++ позволяет указать более одного определения для функции
имени или оператора
в той же области, что называется функция перегрузки
и перегрузки операторов
соответственно. Перегруженное объявление представляет собой объявление, объявленное с тем же именем, что и ранее объявленное объявление в той же области видимости, за исключением того, что обе декларации имеют разные аргументы и, очевидно, другое определение (реализация). Когда вы вызываете перегруженную функцию
или оператор
, компилятор определяет наиболее подходящее определение для использования, сравнивая типы аргументов, которые вы использовали для вызова функции или оператора, с типами параметров, указанными в определениях. Процесс выбора наиболее подходящей перегруженной функции или оператора называется разрешением перегрузки
. Вы можете иметь несколько определений для одного и того же имени функции в той же области. Определение функции должно отличаться друг от друга по типам и / или количеству аргументов в списке аргументов. Вы не можете перегружать объявления функций, которые отличаются только возвращаемым типом. Ниже приведен пример, когда одна и та же функция print ()
используется для печати разных типов данных -
#include Printing int: 5
Printing float: 500.263
Printing character: Hello C++
Вы можете переопределить или перегрузить большинство встроенных операторов, доступных на C ++. Таким образом, программист может также использовать операторы с определенными пользователем типами. Перегруженные операторы - это функции со специальными именами: ключевое слово «оператор», за которым следует символ для определяемого оператора. Как и любая другая функция, перегруженный оператор имеет тип возврата и список параметров. Box operator+(const Box&);
объявляет оператор добавления, который можно использовать для добавления
двух объектов Box и возвращает конечный объект Box. Большинство перегруженных операторов могут быть определены как обычные функции, не являющиеся членами, или как функции членов класса. В случае, если мы определяем функцию выше как функцию, не являющуюся членом класса, мы должны были бы передать два аргумента для каждого операнда следующим образом: Box operator+(const Box&, const Box&);
Ниже приведен пример, показывающий концепцию оператора при загрузке с использованием функции-члена. Здесь объект передается как аргумент, свойства которого будут доступны с помощью этого объекта, к объекту, который вызовет этот оператор, можно получить доступ с помощью этого
оператора, как описано ниже -
#include Когда приведенный выше код компилируется и выполняется, он производит следующий результат: Volume of Box1: 210
Volume of Box2: 1560
Volume of Box3: 5400
Ниже приведен список операторов, которые могут быть перегружены. Итак, мы уже знаем, как объявлять, определять и использовать функции в программах.
В этой главе речь пойдет об их специальном виде – перегруженных функциях. Две
функции называются перегруженными, если они имеют одинаковое имя, объявлены
в одной и той же области видимости, но имеют разные списки формальных параметров.
Мы расскажем, как объявляются такие функции и почему они полезны. Затем мы рассмотрим
вопрос об их разрешении, т.е. о том, какая именно из нескольких перегруженных
функций вызывается во время выполнения программы. Эта проблема является одной
из наиболее сложных в C++. Тем, кто хочет разобраться в деталях, будет интересно
прочитать два раздела в конце главы, где тема преобразования типов аргументов
и разрешения перегруженных функций раскрывается более подробно. Теперь, научившись объявлять, определять и использовать функции в программах,
познакомимся с перегрузкой
– еще одним аспектом в C++. Перегрузка позволяет
иметь несколько одноименных функций, выполняющих схожие операции над аргументами
разных типов. вызывается операция целочисленного сложения, тогда как вычисление выражения 1.0 + 3.0
осуществляет сложение с плавающей точкой. Выбор той или иной операции производится
незаметно для пользователя. Операция сложения перегружена, чтобы обеспечить
работу с операндами разных типов. Ответственность за распознавание контекста
и применение операции, соответствующей типам операндов, возлагается на компилятор,
а не на программиста. Как и в случае со встроенной операцией сложения, нам может понадобиться набор
функций, выполняющих одно и то же действие, но над параметрами различных типов.
Предположим, что мы хотим определить функции, возвращающие наибольшее из переданных
значений параметров. Если бы не было перегрузки, пришлось бы каждой такой функции
присвоить уникальное имя. Например, семейство функций max() могло бы выглядеть
следующим образом: Int i_max(int, int);
int vi_max(const vector Однако все они делают одно и то же: возвращают наибольшее из значений параметров.
С точки зрения пользователя, здесь лишь одна операция – вычисление максимума,
а детали ее реализации большого интереса не представляют. Int ix = max(j, k);
vector Этот подход оказывается чрезвычайно полезным во многих ситуациях. В C++ двум или более функциям может быть дано одно и то же имя при условии,
что их списки параметров различаются либо числом параметров, либо их типами.
В данном примере мы объявляем перегруженную функцию max(): Int max (int, int);
int max(const vector Для каждого перегруженного объявления требуется отдельное определение функции
max() с соответствующим списком параметров. Перегруженные функции не могут различаться лишь типами возвращаемого значения;
если списки параметров двух функций разнятся только подразумеваемыми по умолчанию
значениями аргументов, то второе объявление считается повторным:
// объявления одной и той же функции
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 важен только внутри определения функции: он показывает,
что в теле функции запрещено изменять значение параметра. Однако аргумент, передаваемый
по значению, можно использовать в теле функции как обычную инициированную переменную:
вне функции изменения не видны. (Способы передачи аргументов, в частности передача
по значению, обсуждаются в разделе 7.3 .) Добавление
спецификатора const к параметру, передаваемому по значению, не влияет на его
интерпретацию. Функции, объявленной как f(int), может быть передано любое значение
типа int, равно как и функции f(const int). Поскольку они обе принимают одно
и то же множество значений аргумента, то приведенные объявления не считаются
перегруженными. f() можно определить как Void f(int i) { }
Void f(const int i) { }
Наличие двух этих определений в одной программе – ошибка, так как одна и та
же функция определяется дважды.
// объявляются разные функции
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 Приведем еще один пример. Следующие пять функций-членов 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 Поскольку каждый класс определяет собственную область видимости, функции, являющиеся
членами двух разных классов, не перегружают друг друга. (Функции-члены класса
описываются в главе 13. Разрешение перегрузки для функций-членов класса рассматривается
в главе 15.)
#include Использование using-объявлений и using-директив помогает сделать члены пространства
имен доступными в других областях видимости. Эти механизмы оказывают определенное
влияние на объявления перегруженных функций. (Using-объявления и using-директивы
рассматривались в разделе 8.6 .) Каким образом using-объявление сказывается на перегрузке функций? Напомним,
что оно вводит псевдоним для члена пространства имен в ту область видимости,
в которой это объявление встречается. Что делают такие объявления в следующей
программе? Namespace libs_R_us {
int max(int, int);
int max(double, double);
extern void print(int); Первое 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).
Если разрешить пользователю избирательно включать в область видимости лишь одну
из нескольких перегруженных функций, то поведение программы становится непредсказуемым.
#include Using-объявление добавляет в глобальную область видимости два объявления: для
print(int) и для print(double). Они являются псевдонимами в пространстве libs_R_us
и включаются в множество перегруженных функций с именем print, где уже находится
глобальная print(const string &). При разрешении перегрузки print в fooBar
рассматриваются все три функции. Namespace libs_R_us {
void print(int);
void print(double);
}
void print(int);
using libs_R_us::print; // ошибка: повторное объявление print(int)
void fooBar(int ival) Мы показали, как связаны using-объявления и перегруженные функции. Теперь рассмотрим
особенности применения using-директивы. Using-директива приводит к тому, что
члены пространства имен выглядят объявленными вне этого пространства, добавляя
их в новую область видимости. Если в этой области уже есть функция с тем же
именем, то происходит перегрузка. Например:
#include Это верно и в том случае, когда есть несколько 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 в глобальной области видимости
включает функции print(int), print(double) и print(long double). Все они рассматриваются
в main() при разрешении перегрузки, хотя первоначально были определены в разных
пространствах имен. В разделе 7.7 мы видели, что директиву связывания 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 функция 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(vector Поскольку функция ff() перегружена, одного инициализатора &ff недостаточно
для выбора правильного варианта. Чтобы понять, какая именно функция инициализирует
указатель, компилятор ищет в множестве всех перегруженных функций ту, которая
имеет тот же тип возвращаемого значения и список параметров, что и функция,
на которую ссылается указатель. В нашем случае будет выбрана функция ff(unsigned
int). Extern void ff(vector Присваивание работает аналогично. Если значением указателя должен стать адрес
перегруженной функции, то для выбора операнда в правой части оператора присваивания
используется тип указателя на функцию. И если компилятор не находит функции,
в точности соответствующей нужному типу, он выдает сообщение об ошибке. Таким
образом, преобразование типов между указателями на функции никогда не производится. Matrix calc(const matrix &);
int calc(int, int);
int (*pc1)(int, int) = 0; При использовании перегрузки складывается впечатление, что в программе можно
иметь несколько одноименных функций с разными списками параметров. Однако это
лексическое удобство существует только на уровне исходного текста. В большинстве
систем компиляции программы, обрабатывающие этот текст для получения исполняемого
кода, требуют, чтобы все имена были различны. Редакторы связей, как правило,
разрешают внешние ссылки лексически. Если такой редактор встречает имя print
два или более раз, он не может различить их путем анализа типов (к этому моменту
информация о типах обычно уже потеряна). Поэтому он просто печатает сообщение
о повторно определенном символе print и завершает работу. Зачем может понадобиться объявлять перегруженные функции? Как нужно объявить перегруженные варианты функции error(), чтобы были корректны
следующие вызовы: Int index;
int upperBound;
char selectVal;
// ...
error("Array out of bounds: ", index, upperBound);
error("Division by zero");
error("Invalid selection", selectVal);
Объясните, к какому эффекту приводит второе объявление в каждом из приведенных
примеров:
(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);
Какая из следующих инициализаций приводит к ошибке? Почему?
(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() { Здесь в ходе процесса разрешения перегрузки в зависимости от типа T определяется,
будет ли при обработке выражения f(t1,t2) вызвана функция f(int,int) или f(float,float)
или зафиксируется ошибка. Void f();
void f(int);
void f(double, double = 3.4);
void f(char *, char *);
void main() { При разрешении перегрузки функции выполняются следующие шаги: Рассмотрим последовательно каждый пункт. В нашем примере есть две устоявших функции, которые могут быть вызваны с приведенными
аргументами: Если после второго шага не нашлось устоявших функций, то вызов считается ошибочным.
В таких случаях мы говорим, что имеет место отсутствие соответствия. Что происходит на последнем (третьем) шаге процесса разрешения перегрузки функции? На втором шаге процесса разрешения перегрузки функции компилятор идентифицирует
и ранжирует преобразования, которые следует применить к каждому фактическому
аргументу вызванной функции для приведения его к типу соответствующего формального
параметра любой из устоявших функций. Ранжирование может дать один из трех возможных
результатов: Для установления точного соответствия тип фактического аргумента необязательно
должен совпадать с типом формального параметра. К аргументу могут быть применены
некоторые тривиальные преобразования, а именно: (Подробнее они рассмотрены ниже.)Категория соответствия с преобразованием типа
является наиболее сложной. Необходимо рассмотреть несколько видов такого приведения:
расширение типов (promotions), стандартные преобразования и определенные пользователем
преобразования. (Расширения типов и стандартные преобразования изучаются в этой
главе. Определенные пользователем преобразования будут представлены позднее,
после детального рассмотрения классов; они выполняются конвертером, функцией-членом,
которая позволяет определить в классе собственный набор “стандартных” трансформаций.
В главе 15 мы познакомимся с такими конвертерами и с тем, как они влияют на
разрешение перегрузки функций.) Самый простой случай возникает тогда, когда типы фактических аргументов совпадают
с типами формальных параметров. Например, есть две показанные ниже перегруженные
функции max(). Тогда каждый из вызовов max() точно соответствует одному из объявлений: Int max(int, int);
double max(double, double);
int i1;
void calc(double d1) { Перечислимый тип точно соответствует только определенным в нем элементам перечисления,
а также объектам, которые объявлены как принадлежащие к этому типу: Enum Tokens { INLINE = 128; VIRTUAL = 129; };
Tokens curTok = INLINE;
enum Stat { Fail, Pass };
extern void ff(Tokens); Выше уже упоминалось, что фактический аргумент может точно соответствовать
формальному параметру, даже если для приведения их типов необходимо некоторое
тривиальное преобразование, первое из которых – преобразование l-значения в
r-значение. Под l-значением понимается объект, удовлетворяющий следующим условиям: Напротив, r-значение – это выражение, значение которого вычисляется, или выражение,
обозначающее временный объект, для которого нельзя получить адрес и значение
которого нельзя модифицировать. Вот простой пример: Int calc(int);
int main() {
int lval, res;
lval = 5; // lvalue: lval; rvalue: 5 В первом операторе присваивания переменная lval – это l-значение, а литерал
5 – r-значение. Во втором операторе присваивания res – это l-значение, а временный
объект, в котором хранится результат, возвращаемый функцией calc(), – это r-значение. Int obj1;
int obj2;
int main() { Здесь obj1 и obj2 – это l-значения. Однако для выполнения сложения в функции
main() из переменных obj1 и obj2 извлекаются их значения. Действие, состоящее
в извлечении значения объекта, представленного выражением вида l-значение, называется
преобразованием l-значения в r-значение.
#include Так как аргумент в вызове print(color) передается по значению, то производится
преобразование l-значения в r-значение для извлечения значения color и передачи
его в функцию с прототипом print(string). Однако несмотря на то, что такое приведение
имело место, считается, что фактический аргумент color точно соответствует объявлению
print(string).
#include В вызове ниже li – это l-значение, представляющее объект list List Сопоставление li с параметром-ссылкой считается точным соответствием. Int ai;
void putValues(int *);
int main() { Перед вызовом функции putValues() массив преобразуется в указатель, в результате
чего фактический аргумент ai (массив из трех целых) приводится к указателю на
int. Хотя формальным параметром функции putValues() является указатель и фактический
аргумент при вызове преобразован, между ними устанавливается точное соответствие. Int lexicoCompare(const string &, const string &);
typedef int (*PFI)(const string &, const string &); Перед вызовом sort() применяется преобразование функции в указатель, которое
приводит аргумент lexicoCompare от типа “функция” к типу “указатель на функцию”.
Хотя формальным параметром функции является указатель, а фактическим – имя функции
и, следовательно, было произведено преобразование функции в указатель, считается,
что фактический аргумент точно третьему формальному параметру функции sort(). Int a = { 4454, 7864, 92, 421, 938 };
int *pi = a;
bool is_equal(const int * , const int *);
void func(int *parm) {
// точное соответствие между pi и parm: преобразование спецификаторов Перед вызовом функции is_equal() фактические аргументы pi и parm преобразуются
из типа “указатель на int” в тип “указатель на const int”. Эта трансформация
заключается в добавлении спецификатора const к адресуемому типу, поэтому относится
к категории преобразований спецификаторов. Несмотря на то, что функция ожидает
получить два указателя на const int, а фактические аргументы являются указателями
на int, считается, что точное соответствие между формальными и фактическими
параметрами функции is_equal() установлено. Extern void takeCI(const int);
int main() { Хотя формальный параметр функции takeCI() имеет тип const int, а вызывается
она с аргументом ii типа int, преобразование спецификаторов не производится:
есть точное соответствие между фактическим аргументом и формальным параметром. Extern void init(int *const);
extern int *pi;
int main() { Спецификатор const при формальном параметре функции init() относится к самому
указателю, а не к типу, который он адресует. Поэтому компилятор при анализе
преобразований, которые должны быть применены к фактическому аргументу, не учитывает
этот спецификатор. К аргументу pi не применяется преобразование спецификатора:
считается, что этот аргумент и формальный параметр точно соответствуют друг
другу. Extern void ff(int);
extern void ff(void *);
Ff(0xffbc); // вызывается ff(int)
будет точно соответствовать ff(int), хотя литерал 0xffbc записан в виде шестнадцатеричной
константы. Программист может заставить компилятор вызвать функцию ff(void *),
если явно выполнит операцию приведения типа: Ff(reinterpret_cast Если к фактическому аргументу применяется такое приведение, то он приобретает
тип, в который преобразуется. Явные приведения типов помогают в управлении процессом
разрешения перегрузки. Например, если при разрешении перегрузки получается неоднозначный
результат (фактические аргументы одинаково хорошо соответствуют двум или более
устоявшим функциям), то для устранения неоднозначности можно применить явное
приведение типа, заставив компилятор выбрать конкретную функцию. Под расширением типа понимается одно из следующих преобразований: Подобное расширение применяется, когда тип фактического аргумента совпадает
с одним из только что перечисленных типов, а формальный параметр относится к
соответствующему расширенному типу: Extern void manip(int);
int main() { Символьный литерал имеет тип char. Он расширяется до int. Поскольку расширенный
тип соответствует типу формального параметра функции manip(), мы говорим, что
ее вызов требует расширения типа аргумента. Extern void print(unsigned int);
extern void print(int);
extern void print(char);
unsigned char uc; Для аппаратной платформы, на которой unsigned char занимает один байт памяти,
а int – четыре байта, расширение преобразует unsigned char в int, так как с
его помощью можно представить все значения типа unsigned char. Для такой машинной
архитектуры из приведенного в примере множества перегруженных функций наилучшее
соответствие аргументу типа unsigned char обеспечивает print(int). Для двух
других функций установление соответствия требует стандартного приведения. Enum Stat (Fail, Pass);
extern void 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.
#include При первом обращении к format() фактический аргумент расширяется до типа int,
так как для представления типа e1 используется char, и, следовательно, вызывается
перегруженная функция format(int). При втором обращении тип фактического аргумента
e2 представлен типом unsigned int и аргумент расширяется до unsigned int, из-за
чего вызывается перегруженная функция format(unsigned int). Поэтому следует
помнить, что поведение двух перечислений по отношению к процессу разрешения
перегрузки может быть различным и зависеть от значений элементов, определяющих,
как происходит расширение типа. Имеется пять видов стандартных преобразований, а именно: Вот несколько примеров: Extern void print(void*);
extern void print(double);
int main() { Преобразования, относящиеся к группам 1, 2 и 3, потенциально опасны, так как
целевой тип может и не обеспечивать представления всех значений исходного. Например,
с помощью float нельзя адекватно представить все значения типа int. Именно по
этой причине трансформации, входящие в эти группы, отнесены к категории стандартных
преобразований, а не расширений типов. Int i;
void calc(float);
int main() {
calc(i); // стандартное преобразование между целым типом и типом с
// плавающей точкой потенциально опасно в зависимости от
// значения i
return 0;
}
При вызове функции calc() применяется стандартное преобразование из целого
типа int в тип с плавающей точкой float. В зависимости от значения переменной
i может оказаться, что его нельзя сохранить в типе float без потери точности. 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 либо используя суффикс, обозначающий, что константа принадлежит к типу float: Manip (3.14F)); // manip(float)
Вот еще несколько примеров неоднозначных вызовов, которые помечаются как ошибки,
поскольку соответствуют нескольким перегруженным функциям: Extern void farith(unsigned int);
extern void farith(float);
int main() { Стандартные преобразования указателей иногда противоречат интуиции. В частности,
значение 0 приводится к указателю на любой тип; полученный таким образом указатель
называется нулевым. Значение 0 может быть представлено как константное выражение
целого типа: Void set(int*);
int main() { Константное выражение 0L (значение 0 типа long int) и константное выражение
0x00 (шестнадцатеричное целое значение 0) имеют целый тип и потому могут быть
преобразованы в нулевой указатель типа int*. Enum EN { zr = 0 };
set(zr); // ошибка: zr нельзя преобразовать в тип int*
Вызов функции set() является ошибкой, так как не существует преобразования
между значением zr элемента перечисления и формальным параметром типа int*,
хотя zr равно 0. Void print(int);
void print(void *);
void set(const char *); При вызове print(int) имеет место точное соответствие, тогда как для вызова
print(void*) необходимо приведение значения 0 к типу указателя. Поскольку соответствие
лучше преобразования, для разрешения этого вызова выбирается функция print(int).
Обращение к set() неоднозначно, так как 0 соответствует формальным параметрам
обеих перегруженных функций за счет применения стандартной трансформации. Раз
обе функции одинаково хороши, фиксируется неоднозначность.
#include Только указатели на типы данных могут быть приведены к типу void* с помощью
стандартного преобразования, с указателями на функции так поступать нельзя: Typedef int (*PFV)();
extern PFV testCases; // массив указателей на функции
extern void reset(void *);
int main() { Фактический аргумент или формальный параметр функции могут быть ссылками. Как
это влияет на правила преобразования типов? Int i;
int& ri = i;
void print(int);
int main() { Фактический аргумент в обоих вызовах имеет тип int. Использование ссылки для
его передачи во втором вызове не влияет на сам тип аргумента. Int i;
int& ri = i;
void calc(double);
int main() { А как влияет на преобразования, применяемые к фактическому аргументу, формальный
параметр-ссылка? Сопоставление дает следующие результаты: Следует отметить, что и преобразование l-значения в r-значение, и инициализация
ссылки считаются точными соответствиями. В данном примере первый вызов функции
приводит к ошибке: Void print(int);
void print(int&);
int iobj; Объект iobj – это аргумент, для которого может быть установлено соответствие
с обеими функциями print(), то есть вызов неоднозначен. То же относится и к
следующей строке, где ссылка ri обозначает объект, соответствующий обеим функциям
print(). С третьим вызовом, однако, все в порядке. Для него print(int&)
не является устоявшей. Целая константа – это r-значение, так что она не может
инициализировать параметр-ссылку. Единственной устоявшей функцией для вызова
print(86) является print(int), поэтому она и выбирается при разрешении перегрузки. Назовите два тривиальных преобразования, допустимых при установлении точного
соответствия. Каков ранг каждого из преобразований аргументов в следующих вызовах функций:
(a) void print(int *, int);
int arr;
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); // вызов функции
Какие из данных вызовов ошибочны из-за того, что не существует преобразования
между типом фактического аргумента и формального параметра:
(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 В разделе 9.2 мы уже упоминали, что процесс разрешения перегрузки функций состоит
из трех шагов: Теперь мы готовы к тому, чтобы изучить эти шаги более детально. Функцией-кандидатом называется функция, имеющая то же имя, что и вызванная.
Кандидаты отыскиваются двумя способами: Таким образом, совокупность кандидатов является объединением множества функций,
видимых в точке вызова, и множества функций, объявленных в том же пространстве
имен, к которому принадлежат типы фактических аргументов. Char* format(int);
void g() {
char *format(double);
char* format(char*);
format(3); // вызывается format(double) Так как format(int), объявленная в глобальной области видимости, скрыта, она
не включается в множество функций-кандидатов. Namespace libs_R_us {
int max(int, int);
double max(double, double);
}
char max(char, char);
void func() Функции 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("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) 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-директиву в локальную область видимости, как
в следующем примере? 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) Итак, множество кандидатов состоит из функций, видимых в точке вызова, включая
и те, которые введены 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) являются введенные using-объявлением внутри display()
функции basicLib::print(int) и basicLib::print(double), поскольку они видимы
в точке вызова. Так как фактический аргумент функции имеет тип matrixLib::matrix,
то функция print(), объявленная в пространстве имен matrixLib, также будет кандидатом.
Каковы функции-кандидаты для print(87)? Только basicLib::print(int) и basicLib::print(double),
видимые в точке вызова. Поскольку аргумент имеет тип int, дополнительное пространство
имен в поисках других кандидатов не рассматривается. Устоявшая функция относится к числу кандидатов. В списке ее формальных параметров
либо то же самое число элементов, что и в списке фактических аргументов вызванной
функции, либо больше. В последнем случае для дополнительных параметров задаются
значения по умолчанию, иначе функцию нельзя будет вызвать с данным числом аргументов.
Чтобы функция считалась устоявшей, должно существовать преобразование каждого
фактического аргумента в тип соответствующего формального параметра. (Такие
преобразования были рассмотрены в разделе 9.3.) Void f();
void f(int);
void f(double);
void f(char*, char*);
int main() { Функция f(int) устояла, так как она имеет всего один формальный параметр, что
соответствует числу фактических аргументов в вызове. Кроме того, существует
стандартное преобразование аргумента типа double в int. Функция f(double) также
устояла; она тоже имеет один параметр типа double, и он точно соответствует
фактическому аргументу. Функции-кандидаты f() и f(char*, 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) посредством преобразования
целых типов. Обратите внимание, что функция-кандидат с несколькими параметрами исключается
из числа устоявших, как только выясняется, что один из фактических аргументов
не может быть приведен к типу соответствующего формального параметра, пусть
даже для всех остальных аргументов такое преобразование существует. В следующем
примере функция min(char *, int) исключается из множества устоявших, поскольку
нет возможности трансформации типа первого аргумента int в тип соответствующего
параметра char *. И это происходит несмотря на то, что второй аргумент точно
соответствует второму параметру. Extern double min(double, double);
extern double min(char*, int);
void func() Если после исключения из множества кандидатов всех функций с несоответствующим
числом параметров и тех, для параметров которых не оказалось подходящего преобразования,
не осталось устоявших, то обработка вызова функции заканчивается ошибкой компиляции.
В таком случае говорят, что соответствия не найдено. Void print(unsigned int);
void print(char*);
void print(char);
int *ip; Наилучшей считается та из устоявших функций, формальные параметры которой наиболее
точно соответствуют типам фактических аргументов. Для любой такой функции преобразования
типов, применяемые к каждому аргументу, ранжируются для определения степени
его соответствия параметру. (В разделе 6.2 описаны поддерживаемые
преобразования типов.) Наилучшей из устоявших называют функцию, для которой
одновременно выполняются два условия: Может оказаться так, что для приведения фактического аргумента к типу соответствующего
формального параметра нужно выполнить несколько преобразований. Так, в следующем
примере Int arr;
void putValues(const int *);
int main() { для приведения аргумента arr от типа “массив из трех int” к типу “указатель
на const int” применяется последовательность преобразований: Поэтому было бы более правильно говорить, что для приведения фактического аргумента
к типу формального параметра устоявшей функции требуется последовательность
преобразований. Поскольку применяется не одна, а несколько трансформаций, то
на третьем шаге процесса разрешения перегрузки функции на самом деле ранжируются
последовательности преобразований. Преобразование l-значения ->
расширение типа или стандартное преобразование ->
преобразование спецификаторов
Термин преобразование l-значения относится к первым трем трансформациям из
категории точных соответствий, рассмотренных в разделе 9.2: преобразование l-значения
в r-значение, преобразование массива в указатель и преобразование функции в
указатель. Последовательность трансформаций состоит из нуля или одного преобразования
l-значения, за которым следует нуль или одно расширение типа или стандартное
преобразование, и наконец нуль или одно преобразование спецификаторов. Для приведения
фактического аргумента к типу формального параметра может быть применено только
одна трансформация каждого вида. Описанная последовательность называется последовательностью стандартных преобразований.
Существует также последовательность определенных пользователем преобразований,
которая связана с функцией-конвертером, являющейся членом класса. (Конвертеры
и последовательности определенных пользователем преобразований рассматриваются
в главе 15.) Каковы последовательности изменений фактических аргументов в следующем примере? Namespace libs_R_us {
int max(int, int);
double max(double, double);
}
// using-объявление Аргументы в вызове функции max() имеют тип char. Последовательность преобразований
аргументов при вызове функции libs_R_us::max(int,int) следующая: 1a. Так как аргументы передаются по значению, то с помощью преобразования l-значения
в r-значение извлекаются значения аргументов c1 и c2. 2a. С помощью расширения типа аргументы трансформируются из char в int. 2b. Стандартное преобразование между целым и плавающим типом приводит аргументы
от типа char к типу double. Ранг первой последовательности – расширение типа (самое худшее из примененных
изменений), тогда как ранг второй – стандартное преобразование. Так как расширение
типа лучше, чем преобразование, то в качестве наилучшей из устоявших для данного
вызова выбирается функция libs_R_us::max(int,int). Поскольку нельзя сказать, какая из этих последовательностей лучше другой, вызов
неоднозначен: Int i, j;
extern long calc(long, long);
extern double calc(double, double);
void jj() {
// ошибка: неоднозначность, нет наилучшего соответствия Преобразование спецификаторов (добавление спецификатора const или volatile
к типу, который адресует указатель) имеет ранг точного соответствия. Однако,
если две последовательности трансформаций отличаются только тем, что в конце
одной из них есть дополнительное преобразование спецификаторов, то последовательность
без него считается лучше. Например: Void reset(int *);
void reset(const int *);
int* pi;
int main() { Последовательность стандартных преобразований, примененная к фактическому аргументу
для первой функции-кандидата reset(int*), – это точное соответствие, требуется
лишь переход от l-значения к r-значению, чтобы извлечь значение аргумента. Для
второй функции-кандидата reset(const int *) также применяется трансформация
l-значения в r-значение, но за ней следует еще и преобразование спецификаторов
для приведения результирующего значения от типа “указатель на int” к типу “указатель
на const int”. Обе последовательности представляют собой точное соответствие,
но неоднозначности при этом не возникает. Так как вторая последовательность
отличается от первой наличием трансформации спецификаторов в конце, то последовательность
без такого преобразования считается лучшей. Поэтому наилучшей из устоявших функций
будет reset(int*). Int extract(void *); int main() { Здесь для вызова есть две устоявших функции: extract(void*) и extract(const
void*). Последовательность преобразований для функции extract(void*) состоит
из трансформации l-значения в r-значение для извлечения значения аргумента,
сопровождаемого стандартным преобразованием указателя: из указателя на int в
указатель на void. Для функции extract(const void*) такая последовательность
отличается от первой дополнительным преобразованием спецификаторов для приведения
типа результата от указателя на void к указателю на const void. Поскольку последовательности
различаются лишь этой трансформацией, то первая выбирается как более подходящая
и, следовательно, наилучшей из устоявших будет функция extract(const void*).
#include В первом вызове инициализация ссылок для вызова любой функции является точным
соответствием. Но этот вызов все же не будет неоднозначным. Так как обе инициализации
одинаковы во всем, кроме наличия дополнительной спецификации const во втором
случае, то инициализация без такой спецификации считается лучше, поэтому перегрузка
будет разрешена в пользу устоявшей функции manip(vector Extern int ff(char*, int);
extern int ff(int, int);
int main() {
ff(0, "a"); // ff(int, int) Функция ff(), принимающая два аргумента типа int, выбирается в качестве наилучшей
из устоявших по следующим причинам: Вот еще один пример: Int compute(const int&, short);
int compute(int&, double);
extern int iobj; Обе функции compute(const int&, short) и compute(int&, double)
устояли. Вторая выбирается в качестве наилучшей по следующим причинам: Наличие аргументов со значениями по умолчанию способно расширить множество
устоявших функций. Устоявшими являются функции, которые вызываются с данным
списком фактических аргументов. Но такая функция может иметь больше формальных
параметров, чем задано фактических аргументов, в том случае, когда для каждого
неуказанного параметра есть некое значение по умолчанию: Extern void ff(int);
extern void ff(long, int = 0);
int main() { Для первого и третьего вызовов функция ff() является устоявшей, хотя передан
всего один фактический аргумент. Это обусловлено следующими причинами: Последний вызов является неоднозначным, поскольку обе устоявших функции могут
быть выбраны, если применить стандартное преобразование к первому аргументу.
Функции ff(int) не отдается предпочтение только потому, что у нее один параметр. Объясните, что происходит при разрешении перегрузки для вызова функции compute()
внутри main(). Какие функции являются кандидатами? Какие из них устоят после
первого шага? Какие последовательности преобразований надо применить к фактическому
аргументу, чтобы он соответствовал формальному параметру для каждой устоявшей
функции? Какая функция будет наилучшей из устоявших? Namespace primerLib {
void compute();
void compute(const void *);
}
using primerLib::compute; Что будет, если using-объявление поместить внутрь main() перед вызовом compute()?
Ответьте на те же вопросы. При определении функций в своих программах вы должны указать тип возвращаемого функцией значения, а также количество параметров и тип каждого из них. В прошлом (если вы программировали на языке С), когда у вас была функция с именем add_values, которая работала с двумя целыми значениями, а вы хотели бы использовать подобную функцию для сложения трех целых значений, вам следовало создать функцию с другим именем. Например, вы могли бы использовать add_two_values иadd_three_values. Аналогично если вы хотели использовать подобную функцию для сложения значений типа float, то вам была бы необходима еще одна функция с еще одним именем. Чтобы избежать дублирования функции, C++ позволяет вам определять несколько функций с одним и тем же именем. В процессе компиляции C++ принимает во внимание количество аргументов, используемых каждой функцией, и затем вызывает именно требуемую функцию. Предоставление компилятору выбора среди нескольких функций называется перегрузкой. В этом уроке вы научитесь использовать перегруженные функции. К концу данного урока вы освоите следующие основные концепции: Перегрузка функций позволяет вам использовать одно и то же имя для нескольких функций с разными типами параметров. Для перегрузки функций просто определите две функции с одним и тем же именем и типом возвращаемого значения, которые отличаются количеством параметров или их типом. Перегрузка функций является особенностью языка C++, которой нет в языке С. Как вы увидите, перегрузка функций достаточно удобна и может улучшить удобочитаемость ваших программ. Перегрузка функций позволяет вашим программам определять несколько функций с одним и тем же именем и типом возвращаемого значения. Например, следующая программа перегружает функцию с именемadd_values. Первое определение функции складывает два значения типаint. Второе определение функции складывает три значения. В процессе компиляции C++ корректно определяет функцию, которую необходимо использовать: #include int add_values(int a,int b) { int add_values (int a, int b, int c) ( { Как видите, программа определяет две функции с именами add_valuesПервая функция складывает два значения типа int, в то время как вторая складывает три значения. Вы не обязаны что-либо предпринимать специально для того, чтобы предупредить компилятор о перегрузке, просто используйте ее. Компилятор разгадает, какую функцию следует использовать, основываясь на предлагаемых программой параметрах. Подобным образом следующая программа MSG_OVR.CPP перегружает функцию show_message. Первая функция с именем show_message выводит стандартное сообщение, параметры ей не передаются. Вторая выводит передаваемое ей сообщение, а третья выводит два сообщения: #include void show_message(void) { void show_message(char *message) { void show_message(char *first, char *second) { { Одним из наиболее общих случаев использования перегрузки является применение функции для получения определенного результата, исходя из различных параметров. Например, предположим, что в вашей программе есть функция с именем day_of_week, которая возвращает текущий день недели (0 для воскресенья, 1 для понедельника, …, 6 для субботы). Ваша программа могла бы перегрузить эту функцию таким образом, чтобы она верно возвращала день недели, если ей передан юлианский день в качестве параметра, или если ей переданы день, месяц и год: int day_of_week(int julian_day) { int day_of_week(int month, int day, int year) { По мере изучения объектно-ориентированного программирования в C++, представленного в следующих уроках, вы будете использовать перегрузку функций для расширения возможностей своих программ. Перегрузка функций улучшает удобочитаемость программ Перегрузка функций C++ позволяет вашим программам определять несколько функций с одним и тем же именем. Перегруженные функции должны возвращать значения одинакового типа*, но могут отличаться количеством и типом параметров. До появления перегрузки функций в C++ программисты языка С должны были создавать несколько функций с почти одинаковыми именами. К сожалению программисты, желающие использовать такие функции, должны были помнить, какая комбинация параметров соответствует какой функции. С другой стороны, перегрузка функций упрощает задачу программистов, требуя, чтобы они помнили только одно имя функции.* Перегруженные функции не обязаны возвращать значения одинакового типа по той причине, что компилятор однозначно идентифицирует функцию по ее имени и набору ее аргументов. Для компилятора функции с одинаковыми именами, но различными типами аргументов - разные функции, поэтому тип возвращаемого значения - прерогатива каждой функции. - Прим.перев. Перегрузка функций позволяет вам указать несколько определений для одной и той же функции. В процессе компиляции C++ определит, какую функцию следует использовать, основываясь на количестве и типе передаваемых параметров. Из данного урока вы узнали, что перегружать функции достаточно просто. Из урока 14 вы узнаете, как ссылки C++ упрощают процесс изменения параметров внутри функций. Однако, прежде чем перейти к уроку 14, убедитесь, что вы изучили следующие основные концепции:Перегрузка функции
Использование перегрузки функции
Преимущества перегрузки функции:
Пример перегрузки функций
Функции с переменным числом параметров
Перегрузка функций в C ++
Перегрузка операторов в C ++
Перегружаемый / Non-overloadableOperators
9.1. Объявления перегруженных функций
Вы уже воспользовались предопределенной перегруженной функцией. Например, для
вычисления выражения
В этой главе мы покажем, как определять собственные перегруженные функции.9.1.1. Зачем нужно перегружать имя функции
Отмеченная лексическая сложность отражает ограничение программной среды: всякое
имя, встречающееся в одной и той же области видимости, должно относиться к уникальной
сущности (объекту, функции, классу и т.д.). Такое ограничение на практике создает
определенные неудобства, поскольку программист должен помнить или каким-то образом
отыскивать все имена. Перегрузка функций помогает справиться с этой проблемой.
Применяя перегрузку, программист может написать примерно так:9.1.2. Как перегрузить имя функции
Если в некоторой области видимости имя функции объявлено более одного раза,
то второе (и последующие) объявление интерпретируется компилятором так:
если списки параметров двух функций одинаковы, но типы возвращаемых значений
различны, то второе объявление считается неправильным (несогласованным с первым)
и помечается компилятором как ошибка:
unsigned int max(int i1, int i2); int max(int i1, int i2);
// ошибка: отличаются только типы
// возвращаемых значений
Однако, если спецификатор const или volatile применяется к параметру указательного
или ссылочного типа, то при сравнении объявлений он учитывается.
void f(int&);
void f(const int&);
9.1.3. Когда не надо перегружать имя функции
};
9.1.4. Перегрузка и область видимости A
{
// отдельная область видимости: скрывает обе реализации print()
extern void print(int);
// ошибка: print(const string &) не видна в этой области
print("Value: ");
print(ival); // правильно: 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-объявление вводит в область видимости функцию с уже
существующим именем? Эти функции выглядят так, как будто они объявлены прямо
в том месте, где встречается using-объявление. Поэтому введенные функции участвуют
в процессе разрешения имен всех перегруженных функций, присутствующих в данной
области видимости:
// перегружают print(const string &)
using libs_R_us::print;
void fooBar(int ival)
{
// print(const string &)
}
Если using-объявление вводит некоторую функцию в область видимости, в которой
уже имеется функция с таким же именем и таким же списком параметров, это считается
ошибкой. С помощью using-объявления нельзя задать псевдоним для функции print(int)
в пространстве имен libs_R_us, если в глобальной области видимости уже есть
print(int). Например:
{
print(ival); // какая print? ::print или libs_R_us::print
}
// 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)
}
print(1); // вызывается IBM::print(int)
print(3.1); // вызывается Disney::print(double)
return 0;
}
Итак, повторим, что перегруженные функции находятся в одной и той же области
видимости. В частности, они оказываются там в результате применения using-объявлений
и using-директив, делающих доступными имена из других областей.9.1.5. Директива extern "C" и перегруженные функции A
В директиве связывания разрешается задать только одну из множества перегруженных
функций. Например, следующая программа некорректна:
// написанной на C, так и из программы, написанной на C++.
// функции C++ обрабатывают параметры, являющиеся классами
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&);
extern BigNum calc(const BigNum&);
Директива связывания не имеет значения при решении, какую функцию вызывать;
важны только типы параметров. Выбирается та функция, которая лучше всего соответствует
типам переданных аргументов:9.1.6. Указатели на перегруженные функции A
void (*pf1)(unsigned int) = &ff;
А что если не найдется функции, в точности соответствующей типу указателя? Тогда
компилятор выдаст сообщение об ошибке:
int (*pc2)(int, double) = 0;
// ...
// правильно: выбирается функция calc(int, int)
pc1 = &calc;
// ошибка: нет соответствия: неверный тип второго параметра
pc2 = &calc;
9.1.7. Безопасное связывание A
Чтобы разрешить эту проблему, имя функции вместе с ее списком параметров декорируется
так, чтобы получилось уникальное внутреннее имя. Вызываемые после компилятора
программы видят только это внутреннее имя. Как именно производится такое преобразование
имен, зависит от реализации. Общая идея заключается в том, чтобы представить
число и типы параметров в виде строки символов и дописать ее к имени функции.
Как было сказано в разделе 8.2 , такое кодирование гарантирует,
в частности, что два объявления одноименных функций с разными списками параметров,
находящиеся в разных файлах, не воспринимаются редактором связей как объявления
одной и той же функции. Поскольку этот способ помогает различить перегруженные
функции на фазе редактирования связей, мы говорим о безопасном связывании.
Декорирование имен не применяется к функциям, объявленным с помощью директивы
extern "C", так как лишь одна из множества перегруженных функций может
быть написана на чистом С. Две функции с различными списками параметров, объявленные
как extern "C", редактор связей воспринимает как один и тот же символ.Упражнение 9.1
Упражнение 9.2
Упражнение 9.3
Упражнение 9.4
9.2. Три шага разрешения перегрузки
f(t1, t2);
return 0;
}
Разрешение перегрузки функции – один и самых сложных аспектов языка C++. Пытаясь
разобраться во всех деталях, начинающие программисты столкнутся с серьезными
трудностями. Поэтому в данном разделе мы представим лишь краткий обзор того,
как происходит разрешение перегрузки, чтобы у вас составилось хоть какое-то
впечатление об этом процессе. Для тех, кто хочет узнать больше, в следующих
двух разделах приводится более подробное описание.
Процесс разрешения перегрузки функции состоит из трех шагов, которые мы покажем
на следующем примере:
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.4. Процесс разрешения используется также при вызовах перегруженной функции-члена
класса и перегруженного оператора. В разделе 15.10 рассматриваются правила разрешения
перегрузки, применяемые к функциям-членам класса, а в разделе 15.11 – правила
для перегруженных операторов. При разрешении перегрузки следует также принимать
во внимание функции, конкретизированные из шаблонов. В разделе 10.8
обсуждается, как шаблоны влияют на такое разрешение.)Упражнение 9.5
9.3. Преобразования типов аргументов A
print("a"); // соответствует print(char);
print("a"); // соответствует print(const char*);
print(a); // соответствует print(unsigned int);
unsigned int a;
print(si); // ошибка: нет соответствия
При выборе лучшей из устоявших функций для данного вызова компилятор ищет функцию,
для которой применяемые к фактическим аргументам преобразования являются “наилучшими”.
Преобразования типов ранжируются следующим образом: точное соответствие лучше
расширения типа, расширение типа лучше стандартного преобразования, а оно, в
свою очередь, лучше определенного пользователем преобразования. Мы еще вернемся
к ранжированию в разделе 9.4, а пока на простых примерах покажем, как оно помогает
выбрать наиболее подходящую функцию.9.3.1. Подробнее о точном соответствии
max(56, i1); // точно соответствует max(int, int);
max(d1, 66.9); // точно соответствует max(double, double);
}
extern void ff(Stat);
extern void ff(int);
int main() {
ff(Pass); // точно соответствует ff(Stat)
ff(0); // точно соответствует ff(int)
ff(curTok); // точно соответствует ff(Tokens)
// ...
}
res = calc(lval);
// lvalue: res
// rvalue: временный объект для хранения значения,
// возвращаемого функцией calc()
return 0;
}
В некоторых ситуациях в контексте, где ожидается значение, можно использовать
выражение, представляющее собой l-значение:
// ...
int local = obj1 + obj2;
return 0;
}
Когда функция ожидает аргумент, переданный по значению, то в случае, если аргумент
является l-значением, выполняется его преобразование в r-значение:
print(color); // точное соответствие: преобразование lvalue
// в rvalue
return 0;
}
При вызове функций не всегда требуется применять к аргументам подобное преобразование.
Ссылка представляет собой l-значение; если у функции есть параметр-ссылка, то
при вызове функция получает l-значение. Поэтому к фактическому аргументу, которому
соответствует формальный параметр-ссылка, описанное преобразование не применяется.
Например, пусть объявлена такая функция:
void print(list
// ...
print(li); // точное соответствие: нет преобразования lvalue в
// rvalue
return 0;
}
Второе преобразование, при котором все же фиксируется точное соответствие, –
это преобразование массива в указатель. Как уже отмечалось в разделе 7.3, параметр
функции никогда не имеет тип массива, трансформируясь вместо этого в указатель
на его первый элемент. Аналогично фактический аргумент типа массива из NT (где
N – число элементов в массиве, а T – тип каждого элемента) всегда приводится
к типу указателя на T. Такое преобразование типа фактического аргумента и называется
преобразованием массива в указатель. Несмотря на это, считается, что фактический
аргумент точно соответствует формальному параметру типа “указатель на T”. Например:
// ...
putValues(ai); // точное соответствие: преобразование массива в
// указатель
return 0;
}
При установлении точного соответствия допустимо также преобразование функции
в указатель. (Оно упоминалось в разделе 7.9 .) Как и
параметр-массив, параметр-функция становится указателем на функцию. Фактический
аргумент типа “функция” также автоматически приводится к типу указателя на функцию.
Такое преобразование типа фактического аргумента и называется преобразованием
функции в указатель. Хотя трансформация производится, считается, что фактический
аргумент точно соответствует формальному параметру. Например:
void sort(string *, string *, PFI);
string as;
int main()
{
// ...
sort(as,
as + sizeof(as)/sizeof(as - 1),
lexicoCompare // точное соответствие
// преобразование функции в указатель
);
return 0;
}
Последнее из перечисленных выше – это преобразование спецификаторов. Оно относится
только к указателям и заключается в добавлении спецификаторов const или volatile
(или обоих) к типу, который адресует данный указатель:
if (is_equal(pi, parm))
// ...
return 0;
}
Преобразование спецификаторов применимо только к типу, который адресует указатель.
Оно не употребляется в случае, когда формальный параметр имеет спецификатор
const или volatile, а фактический аргумент – нет.
int ii = ...;
takeCI(ii); // преобразование спецификаторов не применяется
return 0;
}
Все сказанное верно и для случая, когда аргумент является указателем, а спецификаторы
const или volatile относятся к этому указателю:
// ...
init(pi); // преобразование спецификаторов не применяется
return 0;
}
Первые три из рассмотренных преобразований (l-значения в r-значение, массива
в указатель и функции в указатель) часто называют трансформациями l-значений.
(В разделе 9.4 мы увидим, что хотя и трансформации l-значений, и преобразования
спецификаторов относятся к категории преобразований, не нарушающих точного соответствия,
его степень считается выше в случае, когда необходима лишь первая трансформация.
В следующем разделе мы поговорим об этом несколько подробнее.)
Точное соответствие можно установить принудительно, воспользовавшись явным приведением
типов. Например, если есть две перегруженные функции:9.3.2. Подробнее о расширении типов
manip("a"); // тип char расширяется до int
return 0;
}
Рассмотрим следующий пример:
print(uc); // print(int); для uc требуется только расширение типа
Следующий пример иллюстрирует расширение фактического аргумента перечислимого
типа:
extern void ff(char);
int main() {
// правильно: элемент перечисления Pass расширяется до типа int
ff(Pass); // ff(int)
ff(0); // ff(int)
}
Итак, хотя и e1, и e2 являются перечислениями, их представления различаются.
Из-за этого e1 и e2 расширяются до разных типов:
string format(unsigned int);
int main() {
format(a1); // вызывается format(int)
format(a2); // вызывается format(unsigned int)
return 0;
}
9.3.3. Подробнее о стандартном преобразовании
int i;
print(i); // соответствует print(double);
// i подвергается стандартному преобразованию из int в double
print(&i); // соответствует print(void*);
// &i подвергается стандартному преобразованию
// из int* в void*
return 0;
}
Предполагается, что все стандартные изменения требуют одного объема работы.
Например, преобразование из char в unsigned char не более приоритетно, чем из
char в double. Близость типов не принимается во внимание. Если две устоявших
функции требуют для установления соответствия стандартной трансформации фактического
аргумента, то вызов считается неоднозначным и помечается компилятором как ошибка.
Например, если даны две перегруженные функции:
// каждый из последующих вызовов неоднозначен
farith("a"); // аргумент имеет тип char
farith(0); // аргумент имеет тип int
farith(2uL); // аргумент имеет тип unsigned long
farith(3.14159); // аргумент имеет тип double
farith(true); // аргумент имеет тип bool
}
// преобразование указателя из 0 в int* применяется к аргументам
// в обоих вызовах
set(0L);
set(0x00);
return 0;
}
Но поскольку перечисления не относятся к целым типам, элемент, равный 0, не
приводим к типу указателя:
Следует отметить, что константное выражение 0 имеет тип int. Для его приведения
к типу указателя требуется стандартное преобразование. Если в множестве перегруженных
функций есть функция с формальным параметром типа int, то именно в ее пользу
будет разрешена перегрузка в случае, когда фактический аргумент равен 0:
void set(char *);
int main () {
print(0); // вызывается print(int);
set(0); // неоднозначность
return 0;
}
Последнее из возможных преобразований указателя позволяет привести указатель
любого типа к типу void*, поскольку void* – это родовой указатель на любой тип
данных. Вот несколько примеров:
// ...
reset(pi); // преобразование указателя: int* в void*
/// ...
reset(ps); // преобразование указателя: string* в void*
}
// ...
reset(textCases); // ошибка: нет стандартного преобразования
// между int(*)() и void*
return 0;
}
9.3.4. Ссылки
Рассмотрим, что происходит, когда ссылкой является фактический аргумент. Его
тип никогда не бывает ссылочным. Аргумент-ссылка трактуется как l-значение,
тип которого совпадает с типом соответствующего объекта:
print(i); // аргумент - это lvalue типа int
print(ri); // то же самое
return 0;
}
Стандартные преобразования и расширения типов, рассматриваемые компилятором,
одинаковы для случаев, когда фактический аргумент является ссылкой на тип T
и когда он сам имеет такой тип. Например:
calc(i); // стандартное преобразование между целым типом
// и типом с плавающей точкой
calc(ri); // то же самое
return 0;
}
// ...
swap(i1, i2); // правильно: вызывается swap(int &, int &)
// ...
return 0;
}
Вот еще один пример, в котором между формальным параметром-ссылкой и фактическим
аргументом нет соответствия:
takeB(giveB()); // ошибка: параметр должен быть типа const B &
return 0;
}
В обоих случаях мы видим, что если формальный параметр-ссылка имеет спецификатор
const, то между ним и фактическим аргументом может быть установлено точное соответствие.
int &ri = iobj;
int main() {
print(iobj); // ошибка: неоднозначность
print(ri); // ошибка: неоднозначность
print(86); // правильно: вызывается print(int)
return 0;
}
Короче говоря, если формальный параметр представляет собой ссылку, то для фактического
аргумента точное соответствие устанавливается, если он может инициализировать
ссылку, и не устанавливается в противном случае.Упражнение 9.6
Упражнение 9.7
Упражнение 9.8
list
9.4. Детали разрешения перегрузки функций
9.4.1. Функции-кандидаты
void f();
void f(int);
void f(double, double = 3.4);
void f(char*, char*);
int main() {
f(5.6); // для разрешения этого вызова есть четыре кандидата
return 0;
}
NS::C obj;
int main() {
// в точке вызова не видна ни одна из функций takeC()
takeC(cobj); // правильно: вызывается NS::takeC(C&),
// потому что аргумент имеет тип NS::C, следовательно,
// принимается во внимание функция takeC(),
// объявленная в пространстве имен NS
return 0;
}
При идентификации множества перегруженных функций, видимых в точке вызова, применимы
уже рассмотренные ранее правила.
Функция, объявленная во вложенной области видимости, скрывает, а не перегружает
одноименную функцию во внешней области. В такой ситуации кандидатами будут только
функции из во вложенной области, т.е. такие, которые не скрыты при вызове. В
следующем примере функциями-кандидатами, видимыми в точке вызова, являются format(double)
и format(char*):
}
Кандидаты могут быть введены с помощью using-объявлений, видимых в точке вызова:
{
// функции из пространства имен невидимы
// все три вызова разрешаются в пользу глобальной функции max(char, char)
max(87, 65);
max(35.5, 76.6);
max("J", "L");
}
}
max(35.5, 76.6); // вызывается libs_R_us::max(double, double)
max("J", "L"); // вызывается libs_R_us::max(int, int)
}
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(35.5, 76.6); // вызывается libs_R_us::max(double, double)
max("J", "L"); // вызывается::max(int, int)
}
print(mObj); // вызывается maxtrixLib::print(const maxtrix &)
print(87); // вызывается basicLib::print(const maxtrix &)
}
9.4.2. Устоявшие функции
В следующем примере для вызова f(5.6) есть две устоявшие функции: f(int) и f(double).
f(5.6); // 2 устоявшие функции: f(int) и f(double)
return 0;
}
В следующем примере единственной устоявшей функцией для вызова format(3) является
format(double). Хотя кандидата format(char*) можно вызывать с одним аргументом,
не существует преобразования из типа фактического аргумента int в тип формального
параметра char*, а следовательно, функция не может считаться устоявшей.
using libs_R_us::max;
char max(char, char);
void func()
{
// все три функции max() являются устоявшими
max(87, 65); // вызывается using libs_R_us::max(int, int)
}
{
// одна функция-кандидат min(double, double)
min(87, 65); // вызывается min(double, double)
}
class SmallInt { /* ... */ };
SmallInt si;
int main() {
print(ip); // ошибка: нет устоявших функций: соответствие не найдено
print(si); // ошибка: нет устоявших функций: соответствие не найдено
return 0;
}
9.4.3. Наилучшая из устоявших функция
putValues(arr); // необходимо 2 преобразования
// массив в указатель + преобразование спецификатора
return 0;
}
Рангом такой последовательности считается ранг самой плохой из входящих в нее
трансформаций. Как объяснялось в разделе 9.2, преобразования типов ранжируются
следующим образом: точное соответствие лучше расширения типа, а расширение типа
лучше стандартного преобразования. В предыдущем примере оба изменения имеют
ранг точного соответствия. Поэтому и у всей последовательности такой же ранг.
Такая совокупность состоит из нескольких преобразований, применяемых в указанном
порядке:
using libs_R_us::max;
void func()
{
char c1, c2;
max(c1, c2); // вызывается libs_R_us::max(int, int)
}
Последовательность преобразований аргументов при вызове функции libs_R_us::max(double,double)
следующая:
1b. С помощью преобразования l-значения в r-значение извлекаются значения аргументов
c1 и c2.
Если ранжирование последовательностей преобразований аргументов не может выявить
единственной устоявшей функции, то вызов считается неоднозначным. В данном примере
для обоих вызовов calc() требуется такая последовательность:
calc(i, j);
}
reset(pi); // без преобразования спецификаторов лучше:
// выбирается reset(int *)
return 0;
}
Вот еще пример, в котором приведение спецификаторов влияет на то, какая последовательность
будет выбрана:
int extract(const void *);
extract(pi); // выбирается extract(void *)
return 0;
}
Спецификаторы const и volatile влияют также на ранжирование инициализации параметров-ссылок.
Если две такие инициализации отличаются только добавлением спецификатора const
и volatile, то инициализация без дополнительной спецификации считается лучшей
при разрешении перегрузки:
extern vector
manip(vec); // выбирается manip(vector
manip(f()); // выбирается manip(const vector
return 0;
}
Для второго вызова существует только одна устоявшая функция manip(const vector
Разумеется, у функций может быть несколько фактических аргументов. Выбор наилучшей
из устоявших должен производиться с учетом ранжирования последовательностей
преобразований всех аргументов. Рассмотрим пример:
return 0;
}
int main() {
compute(iobj, "c"); // compute(int&, double)
return 0;
}
9.4.4. Аргументы со значениями по умолчанию
ff(2L); // соответствует ff(long, 0);
ff(0, 0); // соответствует ff(long, int);
ff(0); // соответствует ff(int);
ff(3.14); // ошибка: неоднозначность
}
Упражнение 9.9
void compute(int);
void compute(double, double = 3.4);
void compute(char*, char* = 0);
int main() {
compute(0);
return 0;
}
ПЕРВОЕ ЗНАКОМСТВО С ПЕРЕГРУЗКОЙ ФУНКЦИЙ
return(a + b);
)
return(a + b + c);
)
cout << «200 + 801 = » << add_values(200, 801) << endl;
cout << «100 + 201 + 700 = » << add_values(100, 201, 700) << endl;
}
cout << «Стандартное сообщение: » << «Учимся программировать на C++» << endl;
}
cout << message << endl;
}
cout << first << endl;
cout << second << endl;
}
show_message();
show_message(«Учимся программировать на языке C++!»);
show_message(«B C++ нет предрассудков!»,»Перегрузка — это круто!») ;
}КОГДА НЕОБХОДИМА ПЕРЕГРУЗКА
// Операторы
}
// Операторы
}ЧТО ВАМ НЕОБХОДИМО ЗНАТЬ