5.3. Презаредими имена на функции
За дадена
дума се казва, че е презаредима, ако има две или повече различни значения.
Смисълът, в който е употребена думата, се определя в зависимост от контекста.
Ако напишем
static int depth;
значението на static се определя от
обхвата на появата й. Това е или локална статична променлива или е декларирана с
файлов обхват. (В следващия раздел, ние ще въведем едно трето значение на
static, което се отнася за статичен член на клас). Във всеки случай значението
на static напълно се изяснява от контекста, в който се използват. Когато такъв
контекст липсва, казваме, че думата е двусмислена. Такива думи могат да имат две
или повече значения, всяко от които да бъде еднакво възможно.
В
естествените езици двусмислието често е умишлено. В литературата, например,
двусмислието може да обогати нашето разбирането на героите и тематиката на
книгите. Едно лице, може да бъде описано като ограничено (задължено, обвързано)
и решително (непоколебимо, твърдо). Един от героите може да се обърне към друг и
да каже “Хората никога не са справедливи (верни, точни)”. Читателят може да
възприеме различните значения на думата едновременно.
Двусмислието,
обаче, е неподходящо за компилатора. Ако контекстът, в който се появява даден
идентификатор или оператор не е достатъчен за да се изясни значението му
компилаторът издава съобщение за грешка. Двусмислието, обаче, е особено важно
при презаредимите имена на функции, темата на този раздел, както и на
наследствеността при класовете, което е предмет на обсъждане на глави 7 и 8.Защо
да презареждаме имената на функциите?
В С++ на две или повече функции
могат да бъдат дадени едни и същи имена, но с уникална сигнатура, като се
различават по броя или типа на аргументите си. Например,
int max( int,
int);
double max(double, double );
Complex &( const Complex, const
Complex );
Необходима е отделна реализация за всеки уникален набор от
аргументи на max(). Всяка от тях, обаче, изпълнява едно и също общо действие -
връща по-големия от двата аргумента.
От потребителска гледна точка
съществува само една операция, която определя максимална стойност. Детайлите по
реализацията относно начина, по който това се извършва, се отнасят до един
по-широк кръг интереси. Чрез презареждането на функциите потребителят може
просто да напише следното:
int i = max( j, k );
Complex c = max( a, b
);
На английски употребените думи са bound and determined. Изречението
има вида
"People are never just".
Един аналог ни предлага
аритметичният оператор. Изразът 1 + 3 извиква операцията събиране за цели
операнди, докато израза 1.0 + 3.0 извиква различна операция за събиране, която
обработва операнди с плаваща запетая.
За потребителя реализацията на този
механизъм е прозрачна понеже операцията събиране е презаредена така, че да
подава различни свои представители. Компилаторът, а не програмистът, се грижи за
разграничаването на тези различни представители. Презареждането на имена на
функции предлага подобна прозрачност за потребителски дефинирани
функции.
Без способността за презареждане на име на функция на всеки нейн
представител трябва да бъде дадено собствено уникално име. Например, нашето
множество от max() функции ще придобие вида
int max( int, int
);
double fmax( double, double );
Complex &Cmax( const Complex&,
const Complex& );
Тази лексикална сложност не е присъща на проблема
за определяне на по-големия от два обекта от различни типове данни, а по-скоро
отразява едно ограничение на програмната среда - всеки оператор, който се явява
в определен обхват, трябва да бъде уникален. Тази сложност изправя програмиста
пред един практически проблем - той трябва или да помни или да търси всяко
име.
Презареждането на имената освобождава програмиста от тази лексикална
сложност.
Как да презаредим име на функция
Когато едно име на
функция се декларира повече от един път в една програма компилаторът ще
интерпретира втората декларация по следния начин:
- ако както типът за
връщане, така и сигнатурата на двете функции съвпадат напълно, то втората се
разглежда като повторна декларация на първата. Например,
// declares the
same function
extern void print( int *ia, int sz );
void print( int
*array, int size );
Имената на аргументите не са съществени за
сравнението на сигнатурите.
- ако сигнатурите на две функции съвпадат
точно, но типовете за връщане са различни, втората декларация се разглежда като
неправилна повторна декларация на първата и се отбелязва като грешка по време на
компилация. Например,
unsigned int max( int*, int sz );
extern int
max( int *ia, int ); // error
- ако сигнатурите на две функции се
различават по броя или типа на аргументите си, се счита, че двата представителя
на функцията са презаредими. Например,
extern void print( int *, int
);
void print(double *da, int sz );
Една декларация typedef предлага
алтернативно име за съществуващ тип данни; то не създава нов тип данни. Следните
два представителна search() се третират като притежаващи една и съща сигнатура.
Декларацията на втория представител ще предизвика грешка по време на компилация
понеже въпреки, че притежава същата сигнатура, тя има различен тип за
връщане.
// typedef does not introduce a new type
typedef char
*string;
extern int search( string );
extern char search( char ); //
error
Кога да не използваме презареждането на функции
?
Механизмът на презареждането позволява множество от функции, които
изпълняват сходна операция, такава като print(), да бъдат извиквани чрез едно
общо мнемонично име. Свързването с подходящия представител на функцията е
прозрачно за потребителя, като при това отстранява лексикалната сложност,
породена от необходимостта на всяка функция да се дава уникално име, като
iPrint() и iaPrint().
След като разгледахме предимствата на този
механизъм нека кажем кога не се препоръчва той да бъде използван. Един случай
имаме, когато множеството от функции не изпълнява сходна операция. Например, ето
един набор от функции, които работят с обща абстрактна съвкупност от данни.
Първоначално те могат да ни се сторят като евентуални кандидати за
презареждане
void setDate( Date&, int, int, int );
Date&
convertDate( char* );
void printDate( const Date& );
Тези функции
работят с едни и същи типове данни, но не изпълняват една и съща операция. В
този случай лексикалната сложност е програмистко споразумение, което свързва
набора от функции с общия даннов тип. Класовият механизъм в С++ прави този тип
съглашения ненужни. Тези функции трябва да бъдат направени член функции на класа
Date. Например,
class Date{
set( int, int, int );
Date
&convert( char* );
void print();
// ...};
Следният набор от пет
член функции на класа Screen изпълняват различни операции за движение. Отново те
могат да бъдат презареждани чрез едно общо име move().
Screen&
moveHome();
Screen& moveAbs( int, int );
Screen& moveRel( int,
int, char *direction );
Screen& moveX( int );
Screen& moveY( int
);
Последните два представителя не могат да бъдат презареждани; техните
сигнатури са едни и същи. За да осигурим уникалност на сигнатурата трябва да
обединим двете функции в една
Screen& ( int, char xy );
Така
получаваме уникална сигнатура. Освен това, ако някакво проучване покаже, че по
оста x или y промените са по-чести, можем да зададем стойност по
подразбиране
Screen& move( int, char xy = ‘x’);
Проучването
може да покаже също, че най-честото движение е преместване напред с една позиция
по оста х. Ако се поддържа стойност по подразбиране за първият аргумент, обаче
сигнатурата вече не е уникална
Screen& move( int sz = 1, char xy =
‘x’ );
Сега и двете функции move() и moveHome() могат да бъдат викани без
аргументи. Не е необходимо аргумент с инициализатор по подразбиране да бъде
разглеждан, когато се опитваме да съпоставим определен представител при
извикване на презаредима функция.
В този момент програмистът може да
оспори смислеността на презаредимостта на тези две функции. В този случай
изглежда, че презареждането е процес на отхвърляне на ненужна информация.
Въпреки, че движението на курсора е обща операция за тези функции, специфичното
естество на това движение е уникално при всяка от тях. moveHome(), която е един
специален случай на движение на курсора, ни дава друг подобен пример. Името
moveHome() предлага повече информация отколкото move(). Програмата може да се
подобри с едно специално име на функция
inline
Screen&Screen
home(){ return move( 0, 0 ); }
Това освобождава
втората и третата функция за движение. Отново те могат да бъдат презареждани.
Както е по-лесно, обаче, те могат да бъдат обединени в един представител чрез
инициализатор на аргумента по подразбиране
move( int, int, char* = 0
);
Най-добре е програмистът да не мисли, че всяка езикова характеристика
е следващата планина, която трябва да изкачи. Използуването на дадена
характеристика трябва да бъде предизвикано от логиката на приложението, а не
просто от факта, че такава съществува.
Свързване на обръщение към
презаредима функция
Сигнатурата на функцията разграничава един
представител от друг при набор от презаредими функции. Например, ето четири
различни представителя на print()
extern void print( unsigned int
);
extern void print( char* );
extern void print( char );
extern void
print( int );
Едно обръщение към презаредима функция се свързва с
подходящ представител по време на процеса, наречен съпоставяне на аргументите,
за който може да се мисли като за процес на разрешаване на двусмислието.
Съпоставянето на аргументите предизвиква сравняване на фактическите аргументи на
повикването с формалните аргументи на всеки деклариран
представител.
Съществуват три възможни резултата от обръщението към
презаредима функция
1. Успешно съпоставяне. Обръщението се свързва с
подходящ представител. Например, например всяко от следните три обръщения към
print() има като резултат съпоставяне
unsigned a;
print( ‘a’ ); //
matches print(char);
print( "a" ); // matches print(char*);
print( a ); //
matches print(unsigned);
2. Неуспешно съпоставяне. Фактическите аргументи
не могат да бъдат поставени в съответствие с аргументите на дефинираните
представители. Всяко от следните две обръщения към print() има като резултат
неуспешно съпоставяне
int *ip;
SmallInt si; // error no
match
print( si )
print( ip ); // error no match
3. Двусмислено
съпоставяне. Фактическите аргументи могат да бъдат съпоставени с повече от един
дефиниран представител. Следното обръщение е един пример за двусмислие при
съпоставянето, понеже такова може да бъде осъществено с всеки от представителите
на print(), като изключим този, който получава аргумент от тип
char*.
unsigned long u1;
print( u1 ); // error
ambiguous
Съпоставянето може да бъде извършено по един от следните три
начина, в зависимост от приоритета:
1. Точно съпоставяне. Типът на
фактическите аргументи съответства точно на типа на един от дефинираните
представители. Например,
extern ff( int );
extern ff( char* );
f( 0
); // matches
ff( int )0 е от тип int. Обръщението точно съответства на
ff(int).
2. Съпоставяне чрез прилагане на стандартни преобразувания. Ако
не бъде намерено точно съпоставяне се прави опит да се извърши съпотавяне чрез
стандартно преобразуване на фактическия аргумент. Например,
class
X;
extern ff( X& );
extern ff( char* );
ff( 0 ); //
matches
ff(char*)
3. Съпоставяне чрез прилагане на дефинирани от
потребителя преобразувания. Ако не бъде намерено точно съпоставяне или
стандартно преобразуване се използват дефинираното от потребителя.
Например,
class SmallInt
{ operator int();// ...
SmallInt
si;
extern ff( char* );
extern ff( int );
ff( si ); //
matches
ff(int);
operator int() се нарича оператор за преобразуване.
Той позволява на класа да дефинира собствен набор от “стандартни”
преобразувания. Раздел 7.5 разглежда подробно тези дефинирани от потребителя
преобразувания.
Особености на точното съпоставяне
Фактическите
аргументи от тип char, short и float се обработват като специален случай, като
се спазва изискването за точно съпоставяне. Правят се два прегледа на набора от
презаредими функции винаги когато съществува фактически аргумент за една от
тях.
При първия преглед се прави опит за точно съпоставяне на
аргументите. Например,
ff( char );
ff( long );
ff( ‘a’ ); //
ff(char)
Символната константа точно съответства на презаредимия
представител, който има формален аргумент от тип char. Търсенето на съответствие
приключва.
Ако при първия преглед не бъде намерено точно съответствие се
извършва следното
- аргументи от тип char, unsigned char или short се
привеждат към тип int. Аргументи от тип unsigned short се привеждат към тип int
ако машинния размер на int е по-голям от този на short; иначе се првеждат към
тип unsigned int.
- аргументи от тип float се првеждат към тип
double.
При втория преглед се прави опит за намиране на точно
съответствие за аргументите на основата на извършените преобразувания.
Например,
ff( int );
ff( short );
ff( long );
ff( ‘a’ );
//
ff(int);
Символната константа точно съответства на презаредимия
представител, който има формален аргумент от тип int. Съпоставянето на някой от
типовете short или long изисква прилагане на стандартно преобразуване. Търсенето
на съответствие приключва.
Един фактически аргумент от тип int не се
съпоставя точно на формални аргументи от тип char или short. Съответно double не
съответствува точно на аргумент от тип float. Например, дадена е следната двойка
от презаредими функции,
ff( long );
ff( float );
при които
следното обръщение предизвиква двусмислие
ff( 3.14 ); // error
ambiguous