Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
237
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

752

Поскольку в обоих случаях за использованием конвертера следует применение стандартного преобразования, то обе последовательности одинаково хороши и компилятор не может выбрать ни одну из них.

// правильно: явное приведение типа

Спомощью явного приведения типов программист способен задать нужное изменение: long lval = static_cast< int >( num );

Вследствие такого указания выбирается конвертер Token::operator int(), за которым следует стандартное преобразование в long.

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

class SmallInt { public:

SmallInt( const Number & ); // ...

};

class Number { public:

operator SmallInt(); // ...

};

extern void compute( SmallInt ); extern Number num;

тогда, когда два класса определяют преобразования друг в друга. Например: compute( num ); // ошибка: возможно два преобразования

Аргумент num преобразуется в тип SmallInt двумя разными способами: с помощью

конструктора SmallInt::SmallInt(const Number&) либо с помощью конвертера Number::operator SmallInt(). Поскольку оба изменения одинаково хороши, вызов считается ошибкой.

Для разрешения неоднозначности программист может явно вызвать конвертер класса

// правильно: явный вызов устраняет неоднозначность

Number:

compute( num.operator SmallInt() );

Однако для разрешения неоднозначности не следует использовать явное приведение типов, поскольку при отборе преобразований, подходящих для приведения типов, рассматриваются как конвертер, так и конструктор:

compute( SmallInt( num ) ); // ошибка: по-прежнему неоднозначно

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

С++ для начинающих

753

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

15.10.1. Еще раз о разрешении перегрузки функций

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

На третьем шаге этой процедуры выбирается наилучшее соответствие. При этом

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

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

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

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

15.10.2. Функции-кандидаты

Функцией-кандидатом называется функция с тем же именем, что и вызванная.

SmallInt si(15);

Предположим, что имеется такой вызов: add( si, 566 );

Функция-кандидат должна иметь имя add. Какие из объявлений add() принимаются во внимание? Те, которые видимы в точке вызова.

Например, обе функции add(), объявленные в глобальной области видимости, будут

const matrix& add( const matrix &, int ); double add( double, double );

int main() { SmallInt si(15); add( si, 566 ); // ...

кандидатами для следующего вызова:

}

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

если фактический аргумент это объект типа класса, указатель или ссылка на тип

класса либо указатель на член класса и этот тип объявлен в пользовательском

С++ для начинающих

754

пространстве имен, то к множеству функций-кандидатов добавляются

функции,

namespace NS {

class SmallInt { /* ... */ }; class String { /* ... */ };

String add( const String &, const String & );

}

int main() {

//si имеет тип class SmallInt:

//класс объявлен в пространстве имен NS NS::SmallInt si(15);

add( si, 566 ); // NS::add() - функция-кандидат return 0;

объявленные в этом же пространстве и имеющие то же имя, что и вызванная:

}

Аргумент si имеет тип SmallInt, т.е. тип класса, объявленного в пространстве имен NS. Поэтому к множеству функций-кандидатов добавляется add(const String &, const String &), объявленная в этом пространстве имен;

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

namespace NS { class SmallInt {

friend SmallInt add( SmallInt, int ) { /* ... */ }

};

}

int main() { NS::SmallInt si(15);

add( si, 566 ); // функция-друг add() - кандидат return 0;

вызванная функция, то они добавляются к множеству функций-кандидатов:

}

Аргумент функции si имеет тип SmallInt. Функция-друг класса SmallInt add(SmallInt, int) член пространства имен NS, хотя непосредственно в этом пространстве она не объявлена. При обычном поиске в NS функция-друг не будет найдена. Однако при вызове add() с аргументом типа класса SmallInt

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

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

Рассмотрим следующий пример:

С++ для начинающих

755

namespace NS { class SmallInt {

friend SmallInt add( SmallInt, int ) { /* ... */ }

};

class String { /* ... */ };

String add( const String &, const String & );

}

const matrix& add( const matrix &, int ); double add( double, double );

int main() {

//si имеет тип class SmallInt:

//класс объявлен в пространстве имен NS NS::SmallInt si(15);

add( si, 566 ); // вызывается функция-друг return 0;

}

Здесь кандидатами являются:

const matrix& add( const matrix &, int )

глобальные функции: double add( double, double )

функция из пространства имен:

NS::add( const String &, const String & )

функция-друг:

NS::add( SmallInt, int )

При разрешении перегрузки выбирается функция-друг класса SmallInt NS::add( SmallInt, int ) как наилучшая из устоявших: оба фактических аргумента точно соответствуют заданным формальным параметрам.

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

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

15.10.3. Функции-кандидаты для вызова функции в области видимости класса

Когда вызов функции вида

С++ для начинающих

756

calc(t)

встречается в области видимости класса (например, внутри функции-члена), то первая часть множества кандидатов, описанного в предыдущем подразделе (т.е. множество, включающее объявления функций, видимых в точке вызова), может содержать не только функции-члены класса. Для построения такого множества применяется разрешение имени. (Эта тема детально разбиралась в разделах 13.9 – 13.12.)

namespace NS { struct myClass {

void k( int );

static void k( char* ); void mf();

};

int k( double );

};

void h(char);

void NS::myClass::mf() {

h('a');

//

вызывается

глобальная h( char )

k(4);

//

вызывается

myClass::k( int )

Рассмотрим пример:

}

Как отмечалось в разделе 13.11, квалификаторы NS::myClass:: просматриваются в обратном порядке: сначала поиск видимого объявления для имени, использованного в определении функции-члена mf(), ведется в классе myClass, а затем в пространстве имен NS. Рассмотрим первый вызов:

h( 'a' );

При разрешении имени h() в определении функции-члена mf() сначала просматриваются функции-члены myClass. Поскольку функции-члена с таким именем в области видимости этого класса нет, то далее поиск идет в пространстве имен NS. Функции h()нет и там, поэтому мы переходим в глобальную область видимости. Результат глобальная функция h(char), единственная функция-кандидат, видимая в точке вызова.

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

примере построения множества кандидатов для вызова

k( 4 );

Сначала поиск ведется в области видимости класса myClass. При этом найдены две функции-члена k(int) и k(char*). Поскольку множество кандидатов содержит лишь функции, объявленные в той области, где разрешение успешно завершилось, то

пространство имен NS не просматривается и функция k(double) в данное множество не включается.

С++ для начинающих

757

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

15.10.4. Ранжирование последовательностей определенных пользователем преобразований

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

class SmallInt { public:

SmallInt( int );

};

extern void calc( double ); extern void calc( SmallInt ); int ival;

int main() {

calc( ival ); // какая calc() вызывается?

calc(), то какая функция будет вызвана?

}

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

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

существует стандартное преобразование типа фактического аргумента int в тип формального параметра double, а calc(SmallInt) потому, что имеется определенное пользователем преобразование из int в SmallInt, которое использует конструктор SmallInt(int). Следовательно, наилучшей из устоявших функций будет calc(double).

А как сравниваются две последовательности определенных пользователем преобразований? Если в них используются разные конвертеры или разные конструкторы, то обе такие последовательности считаются одинаково хорошими:

С++ для начинающих

758

class Number { public:

operator SmallInt(); operator int();

// ...

};

extern void calc( int ); extern void calc( SmallInt ); extern Number num;

calc( num ); // ошибка: неоднозначность

Устоявшими окажутся и calc(int), и calc(SmallInt); первая поскольку конвертер

Number::operator int()преобразует фактический аргумент типа Number в формальный параметр типа int, а вторая потому, что конвертер Number::operator SmallInt()

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

// явное указание преобразования устраняет неоднозначность

Есть способ разрешить неоднозначность, указав преобразование явно: calc( static_cast< int >( num ) );

Явное приведение типов заставляет компилятор преобразовать аргумент num в тип int с помощью конвертера Number::operator int(). Фактический аргумент тогда будет иметь тип int, что точно соответствует функции calc(int), которая и выбирается в качестве наилучшей.

Допустим, в классе Number не определен конвертер Number::operator int(). Будет ли

// определен только Number::operator SmallInt()

тогда вызов

calc( num ); // по-прежнему неоднозначен?

по-прежнему неоднозначен? Вспомните, что в SmallInt также есть конвертер, способный

class SmallInt { public:

operator int(); // ...

преобразовать значение типа SmallInt в int.

};

Можно предположить, что функция calc() вызывается, если сначала преобразовать

фактический аргумент num из типа Number в тип SmallInt с помощью конвертера

С++ для начинающих

759

Number::operator SmallInt(), а затем результат привести к типу int с помощью

SmallInt::operator SmallInt(). Однако это не так. Напомним, что в

последовательность определенных пользователем преобразований может входит несколько стандартных преобразований, но лишь одно пользовательское. Если конвертер Number::operator int() не определен, то функция calc(int) не считается устоявшей,

поскольку не существует неявного преобразования из типа фактического аргумента num в тип формального параметра int.

Поэтому в отсутствие конвертера Number::operator int() единственной устоявшей функцией будет calc(SmallInt), в пользу которой и разрешается вызов.

Если в двух последовательностях определенных пользователем преобразований употребляется один и тот же конвертер, то выбор наилучшей зависит от

class SmallInt { public:

operator int(); // ...

последовательности стандартных преобразований, выполняемых после его вызова:

void manip( int ); void manip( char );

SmallInt si ( 68 );

main() {

// вызывается manip( int )

manip( si );

};

 

}

 

Как manip(int), так и manip(char) являются устоявшими функциями; первая потому,

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

manip(int) : operator int()->точное соответствие

пользователем преобразований выглядят так:

manip(int) : operator int()->стандартное преобразование

Поскольку в обеих последовательностях используется один и тот же конвертер, то для

определения лучшей из них анализируется ранг последовательности стандартных преобразований. Так как точное соответствие лучше преобразования, то наилучшей из устоявших будет функция manip(int).

Подчеркнем, что такой критерий выбора принимается только тогда, когда в обеих

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

компилятору приходилось выбирать между различными определенными пользователем

С++ для начинающих

760

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

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

class SmallInt { public:

operator int(); operator float(); // ...

за применением конвертера. Например:

void compute( float ); void compute( char );

SmallInt si ( 68 );

main() {

compute( si ); // неоднозначность

};

}

И compute(float), и compute(int) устоявшие функции. compute(float) потому,

что конвертер SmallInt::operator float()преобразует аргумент типа SmallInt в тип параметра float, а compute(char) потому, что SmallInt::operator int()

преобразует аргумент типа SmallInt в тип int, после чего результат стандартно

compute(float) : operator float()->точное соответствие

приводится к типу char. Таким образом, имеются последовательности: compute(char) : operator char()->стандартное преобразование

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

Упражнение 15.12

В классах стандартной библиотеки C++ нет определений конвертеров, а большинство конструкторов, принимающих один параметр, объявлены явными. Однако определено множество перегруженных операторов. Как вы думаете, почему при проектировании было принято такое решение?

Упражнение 15.13

Почему перегруженный оператор ввода для класса SmallInt, определенный в начале этого раздела, реализован не так:

С++ для начинающих

761

istream& operator>>( istream &is, SmallInt &si )

{

return ( is >> is.value );

}

Упражнение 15.14

Приведите возможные последовательности определенных пользователем преобразований

class LongDouble { operator double(); operator float();

};

для следующих инициализаций. Каким будет результат каждой инициализации?

(a)int ex1 = ldObj; extern LongDouble ldObj;

(b)float ex2 = ldObj;

Упражнение 15.15

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

Упражнение 15.16

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

class LongDouble { public:

LongDouble( double ); // ...

};

extern void calc( int ); extern void calc( LongDouble ); double dval;

int main() {

calc( dval ); // какая функция?

функции, и объясните, почему одна из них лучше другой.

}