C++. Бархатный путь. Часть 1 - Совместно используемые функции

ОГЛАВЛЕНИЕ

Совместно используемые функции

Имя функции в заголовке объявления можно рассматривать как индивидуальную характеристику функции. Однако имени функции для её однозначной идентификации недостаточно. Здесь важен комплекс характеристик функции. При этом спецификация возвращаемого функцией значения актуальна лишь в случае, когда выражение вызова функции является частью более сложного выражения. В пределах области действия данного имени функция однозначно идентифицируется именем в сочетании со списком её параметров. Это обстоятельство позволяет реализовывать механизм совместного использования функций.

При объявлении различных функций в C++ можно использовать одни и те же имена. При этом одноимённые функции различаются по спискам параметров. Отсюда становится понятным смысл понятия совместно используемых функций: одни и те же имена функций совместно используются различными списками параметров.

У совместно используемых функций имеется ещё одно название. Такие функции называются перегруженными. Смысл этого названия становится понятным из следующей аналогии. В естественном языке одни и те же глаголы могут обозначать различные действия. Например, можно "ходить по комнате", "ходить под парусом", "ходить конём". В каждом из этих контекстов глагол "ходить" употребляется в новом смысле и в буквальном смысле перегружается разными смыслами.

Механизм совместного использования заключается в том, в ходе трансляции исходного кода переименовываются все функции. Новые имена создаются транслятором на основе старых имен и списков типов параметров. Никакие другие характеристики функция при создании новых имён транслятором не учитываются.

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

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

C++ предлагает компромиссное решение, в основе которого лежит так называемый алгоритм декодирования имени. В программе можно объявить несколько одноименных функций:

int max(int,int);
int max(int*,int);
int max(int,int*);
int max(int*,int*);

и при этом в процессе трансляции, к имени каждой из объявленных функций будет прибавлена специальная цепочка символов, зависящая от типа и порядка параметров функции. Конкретный алгоритм декодирования зависит от транслятора. В соответствии с представленной в книге Б.Бабэ схемой декодирования имён в Borland C++, декодированные имена четвёрки функций будут выглядеть следующим образом:

@max$qii
@max$qpii
@max$qipi
@max$qpipi

Заметим, что при кодировании имён транслятор не использует информацию о типе возвращаемых значений и поэтому пара функций

int max(int*,int*);
int * max(int*,int*);

должна была бы получить одно и то же декодированное имя @max$qpipi, что неизбежно вызвало бы сообщение об ошибке.

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

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

Так что не являются совместно используемыми функции, различающиеся лишь типом возвращающего значения. 

Также не являются совместно используемыми функции, списки параметров которых различаются лишь применением модификаторов const или volatile, или использованием ссылки (эти спецификаторы не используются при модификации имён).

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

Решение относительно вызова совместно используемой функции принимается транслятором и сводится к выбору конкретного варианта функции. Выбор производится в соответствии со специально разработанным алгоритмом, который называется алгоритмом сопоставления параметров.

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

1.Точное сопоставление.

Точное сопоставление предполагает однозначное соответствие количества, типа и порядка значений параметров выражения вызова и параметров в определении функции.

// Произвольная функция, которая возвращает целое значение.
int iFunction(float, char *);
//Объявление пары совместно используемых функций...
extern void FF(char *); //Вариант 1...
extern void FF(int); //Вариант 2...
//Вызов функции.
FF(0);
FF(iFunction(3.14, "QWERTY"));

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

2.Сопоставление с помощью расширения типа.

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

Если ни для одного из вызовов точного сопоставления не произошло, то применяются следующие расширения типа:

  • Параметр типа char, unsigned char или short расширяются до типа int. Параметр типа unsigned short расширяется до типа int, если размер объекта типа int больше размера объекта типа short (это зависит от реализации). Иначе он расширяется до типа unsigned int.
  • Параметр типа float расширяется до типа double.

//Объявление пары совместно используемых функций...

extern void FF(char *);      //Вариант 1...
extern void FF(int); //Вариант 2...
//Вызов функции.
FF('a');

Литера 'a' имеет тип char и значение, допускающее целочисленное расширение. Вызов сопоставляется со вторым вариантом совместно используемой функции.

3.Сопоставление со стандартным преобразованием. Применяется в случае неудачи сопоставления по двум предыдущим критериям сопоставления. Фактический параметр преобразуется в соответствии с правилами стандартных преобразований. Стандартное преобразование типа реализует следующие варианты сопоставления значений параметров в выражениях вызова и параметров объявления:

  • любой целочисленный тип параметра выражения вызова сопоставляется с любым целочисленным типом параметра, включая unsigned,
  • значение параметра, равное нулю, сопоставляется с параметром любого числового типа, а также с параметром типа указатель, а значение параметра типа указатель на объект (любого типа) будет сопоставляться с формальным параметром типа void*.
//Объявление пары совместно используемых функций...
extern void FF(char *); //Вариант 1...
extern void FF(float); //Вариант 2...
//Вызов функции.
FF(0);

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

Можно представить шкалу соответствия типа параметров в выражениях вызова параметрам множества совместно используемых функций. При этом:

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

В качестве примера рассмотрим следующие ситуации сопоставления:

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

extern void FF(unsgned int); //Вариант 1...
extern void FF(char*); //Вариант 2...
extern void FF(char); //Вариант 3...
extern void FF(int); //Вариант 4...

И ещё несколько переменных различных типов...

unsigned int iVal;
int *p_iVal;
unsigned long ulVal;
Рассмотрим вызовы функций.
  • Успешные:
FF('a');
//Успешное сопоставление с вариантом 3.
FF("iVal");
//Успешное сопоставление с вариантом 2.
FF(iVal);
//Успешное сопоставление с вариантом 1.
  • Неудачные:

FF(p_iVal);
//Сопоставления нет.
FF(ulVal);
/*
Поскольку по правилам стандартного преобразования тип unsigned long,
пусть с потерей информации, но всё же может быть преобразован в
любой целочисленный тип, сопоставление окажется неуспешным по причине
своей неоднозначности. Сопоставление происходит со всеми вариантами
функции за исключением функции, имеющей тип char*.
*/

Решение относительно вызова совместно используемой функции с несколькими параметрами принимается на основе алгоритма сопоставления параметров к каждому из параметров вызова функции. При этом применяется так называемое правило пересечения. Согласно этому правилу, из множества совместно используемых функций выбирается функция, для которой разрешение каждого параметра будет НЕ ХУЖЕ (баллы по шкале соответствия), чем для всего множества совместно используемых функций, и ЛУЧШЕ (баллы по шкале соответствия), чем для всех остальных функций, хотя бы для одного параметра. Например:

extern MyFFF(char*, int);
extern MyFFF(int, int);
MyFFF(0, 'a');


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

  1. Сопоставление первого фактического параметра вызова функции и первого параметра второй функции оценивается высшим баллом по шкале соответствия параметров, поскольку константа 0 точно сопоставляется с формальным параметром типа int.
  2. Второй параметр вызова сопоставляется со вторым формальным параметром обеих функций. При этом литера 'a' имеет тип char и значение, допускающее целочисленное расширение. Таким образом, имеет место сопоставление с помощью расширения типа.

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

Известно, что значением выражения, состоящего из имени функции, является адрес данной функции. Подобные выражения для перегруженных функций недопустимы в силу своей неоднозначности. Транслятор просто не представляет, адрес какой из функций следует определять. Однако всё же определение адреса совместно используемых функций возможно. Это можно осуществить в контексте определения и инициализации указателя на функцию. Необходимую для выбора соответствующей перегруженной функции информацию транслятор получает из спецификации соответствующего указателя.

char* MyFF1 (int,int,int*,float);
char* MyFF1 (int,int*,float);
/* Прототипы перегруженных функций. */
:::::
char* MyFF1 (int key1, int key2, int* pVal, float fVal) {/* ... */}
char* MyFF1 (int XX, int* pXX, float FF) {/* ... */}
/* Определения перегруженных функций. */
:::::
char* (*fPointer1) (int,int,int*,float) = MyFF1;
/* Определение и инициализация указателя на первую функцию.
Транслятор делает правильный выбор. */
char* (*fPointer2) (int,int*,float);
/* Определение указателя на вторую функцию. */
fPointer2 = MyFF1;
/* И опять транслятор правильно выбирает соответствующую функцию. */
fPointer1(1,2,NULL,3.14);
fPointer2(1,NULL,3.14);
/* Вызовы функций по указателю. */

По крайней мере, в Borland C++ 4.5, аналогичная инициализация параметров-указателей на функции адресами совместно используемых функций недопустима. Можно предположить, что на этом этапе у транслятора нет ещё полной и достоверной информации обо всех совместно используемых функциях программы.

В разделе, посвящённом указателям на функции, в качестве примера была приведена функция, у которой в качестве параметра был указатель на функцию. Так вот попытка предварительной инициализации параметра-указателя адресом совместно используемой функции недопустимо. Соответствующие ограничания накладываются и на использование значения по умолчанию этого параметра при вызове функции.

void MonitorF(int,int,int*,float,char*(*)(int,int,int*,float)=MyFF1);
/*
Транслятор утверждает, что имя этих функций двусмысленно в
контексте инициализации.
*/
MonitorF(9,9,pIval,9.9);
/*
Использование значения параметра по умолчанию также невозможно.
*/
void MonitorF(int,int,int*,float,char*(*)(int,int,int*,float));
MonitorF(11,11,pIval,11.11,MyFF1);
/*
При явном указании имени функции в операторе вызова транслятор
однозначно идентифицирует функцию.
*/