С++ для начинающих - Учебное пособие

15. перегруженные операторы и определенные пользователем преобразования

В  главе  15  мы  рассмотрим  два  вида  специальных   функций:  перегруженные операторы и определенные пользователем преобразования. Они дают возможность употреблять объекты классов в выражениях так же интуитивно, как и объекты встроенных типов. В этой главе мы сначала изложим общие концепции проектирования   перегруженных  операторов.  Затем  представим  понятие  друзей класса со специальными правами доступа и обсудим, зачем они применяются, обратив особое внимание на то, как реализуются некоторые перегруженные операторы:  присваивание,  взятие  индекса,  вызов,  стрелка  для  доступа  к  члену класса, инкремент и декремент, а также специализированные для класса операторы new и delete. Другая категория специальных функций, которая рассматривается в этой  главе, –  это  функции  преобразования  членов  (конвертеры),  составляющие набор стандартных преобразований для типа класса. Они неявно применяются компилятором, когда объекты классов используются в качестве фактических аргументов функции или операндов встроенных или перегруженных операторов. Завершается  глава  развернутым  изложением  правил  разрешения  перегрузки функций  с  учетом  передачи  объектов  в  качестве  аргументов,  функций-членов класса и перегруженных операторов.

15.1. Перегрузка операторов

В предыдущих главах мы уже показывали, что перегрузка операторов позволяет программисту вводить собственные версии предопределенных  операторов  (см. главу 4) для операндов  типа классов. Например, в классе String из раздела 3.15 задано много перегруженных операторов. Ниже приведено его определение:

 

#include <iostream>

class String;

istream& operator>>( istream &, const String & );

ostream& operator<<( ostream &, const String & );

class String {

public:

// набор перегруженных конструкторов

// для автоматической инициализации

String( const char* = 0 ); String( const String & );

// деструктор: автоматическое уничтожение

~String();

// набор перегруженных операторов присваивания String& operator=( const String & ); String& operator=( const char * );

// перегруженный оператор взятия индекса

char& operator[]( int );

// набор перегруженных операторов равенства

// str1 == str2;

bool operator==( const char * );

bool operator==( const String & );

// функции доступа к членам

int size() { return _size; };

char * c_str() { return _string; }

 

private:

};

 

int        _size;

char *_string;

 

 

В  классе  String есть  три  набора  перегруженных  операторов.  Первый –  это  набор

// набор перегруженных операторов присваивания

String& operator=( const String & );

операторов присваивания:

String& operator=( const char * );

Сначала   идет  копирующий   оператор   присваивания.   (Подробно  они  обсуждались  в разделе  14.7.)  Следующий  оператор  поддерживает  присваивание  C-строки  символов

String name;

объекту типа String:

name = "Sherlock";     // использование оператора operator=( char * )

(Операторы присваивания, отличные от копирующих, мы рассмотрим в разделе 15.3.)

Во втором наборе есть всего один оператор – взятия индекса:

 

// перегруженный оператор взятия индекса

char& operator[]( int );

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

if ( name[0] != 'S' )

массивы объектов встроенного типа:

cout << "увы, что-то не так ";

(Детально этот оператор описывается в разделе 15.4.)

В третьем наборе определены перегруженные операторы равенства для объектов класса

String.  Программа  может проверить равенство двух таких объектов или объекта и C-

// набор перегруженных операторов равенства

// str1 == str2;

bool operator==( const char * );

строки:

bool operator==( const String & );

Перегруженные операторы позволяют использовать объекты типа класса с операторами, определенными в главе 4, и манипулировать ими так же интуитивно, как объектами встроенных типов. Например, желая определить операцию конкатенации двух объектов класса String, мы могли бы реализовать ее в виде функции-члена concat(). Но почему concat(), а не, скажем, append()? Выбранное нами имя логично и легко запоминается, но пользователь все же может забыть, как мы назвали функцию. Зачастую имя проще запомнить, если определить перегруженный оператор. К примеру, вместо concat() мы назвали  бы  новую операцию  operator+=().  Такой оператор используется  следующим

#include "String.h" int main() {

String name1 "Sherlock"; String name2 "Holmes";

name1 += " ";

name1 += name2;

if (! ( name1 == "Sherlock Holmes" ) )

cout << "конкатенация не сработала ";

образом:

}

Перегруженный оператор объявляется в теле класса точно так же, как обычная функция- член, только его имя состоит из ключевого слова operator, за которым следует один из множества  предопределенных  в  языке  C++  операторов  (см.  табл.  15.1).  Так  можно объявить operator+=() в классе String:

 

class String {

public:

// набор перегруженных операторов += String& operator+=( const String & ); String& operator+=( const char * );

// ... private:

// ...

};

#include <cstring>

inline String& String::operator+=( const String &rhs )

{

// Если строка, на которую ссылается rhs, непуста

if ( rhs._string )

{

String tmp( *this );

// выделить область памяти, достаточную

// для хранения конкатенированных строк

_size += rhs._size;

delete [] _string;

_string = new char[ _size + 1 ];

// сначала скопировать в выделенную область исходную строку

// затем дописать в конец строку, на которую ссылается rhs strcpy( _string, tmp._string );

strcpy( _string + tmp._size, rhs._string );

}

return *this;

}

inline String& String::operator+=( const char *s )

{

// Если указатель s ненулевой

if ( s )

{

String tmp( *this );

// выделить область памяти, достаточную

// для хранения конкатенированных строк

_size += strlen( s );

delete [] _string;

_string = new char[ _size + 1 ];

// сначала скопировать в выделенную область исходную строку

// затем дописать в конец C-строку, на которую ссылается s strcpy( _string, tmp._string );

strcpy( _string + tmp._size, s );

}

return *this;

и определить его следующим образом:

}

 

15.1.1. Члены и не члены класса

Рассмотрим  операторы  равенства  в  нашем  классе String более внимательно.  Первый оператор  позволяет  устанавливать  равенство  двух  объектов,  а  второй –  объекта  и  C-

#include "String.h"

int main() { String flower;

// что-нибудь записать в переменную flower

if ( flower == "lily" ) // правильно

// ... else

if ( "tulip" == flower ) // ошибка

// ...

строки:

}

При  первом  использовании  оператора  равенства  в main() вызывается  перегруженный operator==(const char *) класса   String.   Однако   на   второй   инструкции   if компилятор выдает сообщение об ошибке. В чем дело?

Перегруженный оператор, являющийся членом некоторого класса, применяется только тогда, когда левым операндом служит объект этого класса. Поскольку во втором случае левый операнд не принадлежит к классу String, компилятор пытается найти такой встроенный оператор, для которого левым операндом может быть C-строка, а правым – объект класса String. Разумеется, его не существует, поэтому компилятор говорит об ошибке.

Но можно же создать объект класса String из C-строки с помощью конструктора класса.

Почему компилятор не выполнит неявно такое преобразование:

if ( String( "tulip" ) == flower ) //правильно: вызывается оператор-член

Причина  в  его  неэффективности.  Перегруженные  операторы  не  требуют,  чтобы  оба операнда имели один и тот же тип. К примеру, в классе Text определяются следующие

class Text {

public:

Text( const char * = 0 ); Text( const Text & );

// набор перегруженных операторов равенства

bool operator==( const char * ) const;

bool operator==( const String & ) const;

bool operator==( const Text & ) const;

// ...

операторы равенства:

};

и выражение в main() можно переписать так:

 

if ( Text( "tulip" ) == flower ) // вызывается Text::operator==()

Следовательно,  чтобы  найти  подходящий  для  сравнения  оператор  равенства, компилятору придется просмотреть все определения классов в поисках конструктора, способного  привести  левый операнд  к некоторому  типу класса.  Затем для каждого из таких типов нужно проверить все ассоциированные с ним перегруженные операторы равенства, чтобы понять, может ли хоть один из них выполнить сравнение. А после этого компилятор должен решить, какая из найденных комбинаций конструктора и оператора равенства (если таковые нашлись) лучше всего соответствует операнду в правой части! Если потребовать от компилятора выполнения всех этих действий, то время трансляции программ C++ резко возрастет. Вместо этого компилятор просматривает только перегруженные  операторы,  определенные  как  члены  класса  левого  операнда  (и  его базовых классов, как мы покажем в главе 19).

Разрешается,  однако,  определять  перегруженные  операторы,  не  являющиеся  членами класса.   При  анализе  строки  в  main(),  вызвавшей   ошибку  компиляции,   подобные операторы  принимались  во внимание.  Таким  образом,  сравнение,  в котором  C-строка стоит в левой части, можно сделать корректным, если заменить операторы равенства, являющиеся  членами  класса  String,  на  операторы  равенства,  объявленные в области

bool operator==( const String &, const String & );

видимости пространства имен:

bool operator==( const String &, const char * );

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

flower == "lily"

переписывается компилятором в виде:

flower.operator==( "lily" )

и  на  левый  операнд  flower в  определении  перегруженного  оператора-члена  можно сослаться с помощью this. (Указатель this введен в разделе 13.4.) В случае глобального перегруженного оператора параметр, представляющий левый операнд, должен быть задан явно.

Тогда выражение

flower == "lily"

вызывает оператор

bool operator==( const String &, const char * );

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

 

"tulip" == flower

Мы ведь не определили такой перегруженный оператор:

bool operator==( const char *, const String & );

Но это необязательно. Когда перегруженный оператор является функцией в пространстве имен,  то  как  для  первого,  так  и  для  второго  его  параметра  (для  левого  и  правого операндов)  рассматриваются  возможные  преобразования,  т.е.  компилятор интерпретирует второе использование оператора равенства как

operator==( String("tulip"), flower );

и вызывает для выполнения сравнения следующий перегруженный оператор:

bool operator==( const String &, const String & );

Но тогда зачем мы предоставили второй перегруженный оператор:

bool operator==( const String &, const char * );

Преобразование типа  из C-строки  в класс String может быть применено и к правому операнду. Функция main() будет компилироваться без ошибок, если просто определить в пространстве имен перегруженный оператор, принимающий два операнда String:

bool operator==( const String &, const String & );

bool operator==( const char *, const String & );

Предоставлять ли только этот оператор или еще два:

bool operator==( const String &, const char * );

зависит от того, насколько велики затраты на преобразование из C-строки в String во время выполнения, то есть от “стоимости” дополнительных вызовов конструктора в программах, пользующихся нашим классом String. Если оператор равенства будет часто использоваться  для  сравнения  C-строк  и  объектов  ,  то  лучше  предоставить  все  три варианта. (Мы вернемся к вопросу эффективности в разделе, посвященном друзьям.

Подробнее  о  приведении  к  типу  класса  с  помощью  конструкторов  мы  расскажем  в разделе 15.9; в разделе 15.10 речь пойдет о разрешении перегрузки функций с помощью описанных преобразований, а в разделе 15.12 – о разрешении перегрузки операторов.)

Итак,  на  основе  чего  принимается  решение,  делать  ли  оператор  членом  класса  или членом пространства имен? В некоторых случаях у программиста просто нет выбора:

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

язык требует, чтобы операторы присваивания ("="), взятия индекса ("[]"), вызова ("()") и доступа к членам по стрелке ("->") были определены как члены класса. В противном случае выдается сообщение об ошибке компиляции:

 

// ошибка: должен быть членом класса

char& operator[]( String &, int ix );

(Подробнее оператор присваивания  рассматривается  в разделе 15.3, взятия индекса – в разделе 15.4, вызова – в разделе 15.5, а оператор доступа к члену по стрелке – в разделе

15.6.)

В остальных случаях решение принимает проектировщик класса. Симметричные операторы, например оператор равенства, лучше определять в пространстве имен, если членом класса может быть любой операнд (как в String).

Прежде  чем  закончить  этот  подраздел,  определим  операторы  равенства  для  класса

bool operator==( const String &str1, const String &str2 )

{

if ( str1.size() != str2.size() )

return false;

return strcmp( str1.c_str(), str2.c_str() ) ? false : true ;

}

inline bool operator==( const String &str, const char *s )

{

return strcmp( str.c_str(), s ) ? false : true ;

String в пространстве имен:

}

15.1.2. Имена перегруженных операторов

Перегружать можно только предопределенные операторы языка C++ (см. табл. 15.1).

Таблица 15.1. Перегружаемые операторы

 

+

-

*

/

\%

^

&

|

~

!

,

=

<=

>=

++

--

<< 

>> 

==

!=

&&

||

+=

-=

/=

\%=

^=

&=

|=

*=

<<=

>>=

[]

()

->

->*

new

new[]

delete

delete[]

 

 

 

 

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

// неперегружаемые операторы

Следующие четыре оператора языка C++ не могут быть перегружены:

:: .* . ?:

 

Предопределенное   назначение   оператора   нельзя   изменить   для   встроенных   типов.

Например, не разрешается переопределить встроенный оператор сложения целых чисел

// ошибка: нельзя переопределить встроенный оператор сложения int

так, чтобы он проверял результат на переполнение.

int operator+( int, int );

Нельзя также определять дополнительные операторы для встроенных типов данных, например  добавить  к множеству  встроенных  операций  operator+ для  сложения  двух массивов.

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

Предопределенные    приоритеты    операторов    (см.    раздел    4.13)    изменить    нельзя.

Независимо от типа класса и реализации оператора в инструкции

x == y + z;

всегда сначала выполняется operator+, а затем operator==; однако помощью скобок порядок можно изменить.

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

// некорректно: ! - это унарный оператор

bool operator!( const String &s1, const String &s2 )

{

return ( strcmp( s1.c_str(), s2.c_str() ) != 0 );

компиляции:

}

Для  встроенных  типов  четыре  предопределенных  оператора  ("+",  "-",  "*" и  "&") используются либо как унарные, либо как бинарные. В любом из этих качеств они могут быть перегружены.

Для всех перегруженных  операторов,  за исключением  operator(),  недопустимы аргументы по умолчанию.

15.1.3. Разработка перегруженных операторов

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

 

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

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

isEmpty() становится оператором “ЛОГИЧЕСКОЕ НЕ”, operator!().

isEqual() становится оператором равенства, operator==().

copy() становится оператором присваивания, operator=().

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

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

Такой оператор одинаково хорошо поддерживает несколько различных интерпретаций. Безупречно четкое и обоснованное объяснение того, что делает operator+(), вряд ли устроит  пользователей  класса  String,  полагающих,  что он  служит  для  конкатенации строк. Если семантика перегруженного оператора неочевидна, то лучше его не предоставлять.

Эквивалентность  семантики  составного  оператора  и  соответствующей последовательности  простых  операторов  для  встроенных  типов  (например, эквивалентность оператора +, за которым следует =, и составного оператора +=) должна быть   явно  поддержана   и   для  класса.   Предположим,   для  String определены   как operator+(), так и operator=() для поддержки операций конкатенации и почленного

String s1( "C" ); String s2( "++" );

копирования:

s1 = s1 + s2;    // s1 == "C++"

Но этого недостаточно для поддержки составного оператора присваивания

s1 += s2;

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

Упражнение 15.1

Почему при выполнении следующего сравнения не вызывается перегруженный оператор

operator==(const String&, const String&):

"cobble" == "stone"

Упражнение 15.2

 

Напишите перегруженные операторы неравенства, которые могут быть использованы в

String != String

String != С-строка

таких сравнениях:

C-строка != String

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

Упражнение 15.3

Выявите те функции-члены класса Screen, реализованного в главе 13 (разделы 13.3, 13.4

и 13.6), которые можно перегружать.

Упражнение 15.4

Объясните, почему перегруженные операторы ввода и вывода, определенные для класса

String из раздела 3.15, объявлены как глобальные функции, а не функции-члены.

Упражнение 15.5

Реализуйте перегруженные операторы ввода и вывода для класса Screen из главы 13.

15.2. Друзья

Рассмотрим    еще   раз    перегруженные    операторы    равенства    для    класса    String,

определенные  в  области  видимости  пространства  имен.  Оператор  равенства  для  двух

bool operator==( const String &str1, const String &str2 )

{

if ( str1.size() != str2.size() )

return false;

return strcmp( str1.c_str(), str2.c_str() ) ? false : true;

объектов String выглядит следующим образом:

}

bool String::operator==( const String &rhs ) const

{

if ( _size != rhs._size )

return false;

return strcmp( _string, rhs._string ) ? false : true;

Сравните это определение с определением того же оператора как функции-члена:

}

Нам пришлось модифицировать способ обращения к закрытым членам класса String. Поскольку  новый оператор равенства – это глобальная функция,  а не функция-член,  у него  нет  доступа  к закрытым  членам  класса  String.  Для  получения  размера  объекта String и лежащей в его основе C-строки символов используются функции-члены size() и c_str().

 

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

Объявление друга (оно начинается с ключевого слова friend) встречается только внутри определения класса. Поскольку друзья не являются членами класса, объявляющего дружественные отношения,  то безразлично,  в какой из секций – public,  private или protected –  они  объявлены.  В  примере  ниже  мы  решили  поместить  все  подобные

class String {

friend bool operator==( const String &, const String & );

friend bool operator==( const char *, const String & );

friend bool operator==( const String &, const char * );

public:

// ... остальная часть класса String

объявления сразу после заголовка класса:

};

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

// дружественные операторы напрямую обращаются к закрытым членам

// класса String

bool operator==( const String &str1, const String &str2 )

{

if ( str1._size != str2._size )

return false;

return strcmp( str1._string, str2._string ) ? false : true;

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

inline bool operator==( const String &str, const char *s )

{

return strcmp( str._string, s ) ? false : true;

}

}

// и т.д.

Можно  возразить,  что  в  данном  случае  прямой  доступ  к  членам  _size и  _string необязателен,  так как встроенные функции c_str() и size() столь же эффективны и при этом сохраняют инкапсуляцию, а значит, нет особой нужды объявлять операторы равенства для класса String его друзьями.

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

 

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

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

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

extern ostream& storeOn( ostream &, Screen & );

extern BitMap& storeOn( BitMap &, Screen & );

// ...

class Screen

{

friend ostream& storeOn( ostream &, Screen & );

friend BitMap& storeOn( BitMap &, Screen & );

// ...

он хочет дать неограниченные права доступа:

};

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

class Window; // это всего лишь объявление

class Screen {

friend bool is_equal( Screen &, Window & );

// ...

};

class Window {

friend bool is_equal( Screen &, Window & );

// ...

Объявление функции другом двух классов должно выглядеть так:

};

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

 

class Window;

class Screen {

// copy() - член класса Screen

Screen& copy( Window & );

// ...

};

class Window {

// Screen::copy() - друг класса Window

friend Screen& Screen::copy( Window & );

// ...

};

Screen& Screen::copy( Window & ) { /* ... */ }

Функция-член одного класса не может быть объявлена другом второго, пока компилятор не увидел определения ее собственного класса. Это не всегда возможно. Предположим, что  Screen должен  объявить  некоторые  функции-члены  Window своими  друзьями,  а Window – объявить таким же образом некоторые функции-члена Screen. В таком случае

class Window;

class Screen {

friend class Window;

// ...

весь класс Window объявляется другом Screen:

};

К закрытым членам класса Screen теперь можно обращаться из любой функции-члена

Window.

Упражнение 15.6

Реализуйте операторы ввода и вывода, определенные для класса Screen в упражнении

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

15.3. Оператор =

Присваивание одного объекта другому объекту того же класса выполняется с помощью копирующего оператора присваивания. (Этот специальный случай был рассмотрен в разделе 14.7.)

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

String car ("Volks");

поддержать присваивание C-строки объекту String:

car = "Studebaker";

 

мы предоставляем оператор, принимающий параметр типа const char*. Эта операция

class String {

public:

// оператор присваивания для char*

String& operator=( const char * );

// ... private:

int _size;

char *string;

уже была объявлена в нашем классе:

};

Такой  оператор  реализуется  следующим  образом.  Если  объекту String присваивается нулевой  указатель,  он  становится  “пустым”.  В  противном  случае  ему  присваивается

String& String::operator=( const char *sobj )

{

// sobj - нулевой указатель

if (! sobj ) {

_size = 0;

delete[] _string;

_string = 0;

}

else {

_size = strlen( sobj );

delete[] _string;

_string = new char[ _size + 1 ];

strcpy( _string, sobj );

}

return *this;

копия C-строки:

}

_string ссылается  на  копию  той  C-строки,  на  которую  указывает  sobj.  Почему  на копию? Потому что непосредственно присвоить sobj члену _string нельзя:

_string = sobj; // ошибка: несоответствие типов

sobj – это указатель на const и, следовательно, не может быть присвоен указателю на

“не-const” (см. раздел 3.5). Изменим определение оператора присваивания:

String& String::operator=( const *sobj ) { // ... }

Теперь  _string прямо  ссылается  на  C-строку,  адресованную  sobj.  Однако  при  этом возникают другие проблемы. Напомним, что C-строка имеет тип const char*. Определение параметра как указателя на не-const делает присваивание невозможным:

car = "Studebaker"; // недопустимо с помощью operator=( char *) !

Итак,  выбора  нет.  Чтобы  присвоить  C-строку  объекту  типа  String,  параметр  должен иметь тип const char*.

 

Хранение в _string прямой ссылки на C-строку, адресуемую sobj, порождает и иные сложности.  Мы  не  знаем,  на  что  именно  указывает  sobj.  Это  может  быть  массив

char ia[] = { 'd', 'a', 'n', 'c', 'e', 'r' };

String trap = ia;           // trap._string ссылается на ia

символов, который модифицируется способом, неизвестным объекту String. Например:

ia[3] = 'g'; // а вот это нам не нужно:

// модифицируется и ia, и trap._string

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

Обратите внимание, что в операторе присваивания используется delete. Член _string содержит  ссылку  на  массив  символов,  расположенный  в  хипе.  Чтобы  предотвратить утечку,  память,  выделенная  под старую  строку,  освобождается  с помощью  delete до выделения  памяти  под новую.  Поскольку  _string адресует  массив  символов,  следует использовать версию delete для массивов (см. раздел 8.4).

И последнее замечание об операторе присваивания.  Тип возвращаемого им значения –

это ссылка  на класс String. Почему именно ссылка? Дело в том, что для встроенных

// сцепление операторов присваивания

int iobj, jobj;

типов операторы присваивания можно сцеплять:

iobj = jobj = 63;

Они   ассоциируются    справа    налево,    т.е.   в   предыдущем    примере   присваивания выполняются так:

iobj = (jobj = 63);

Это  удобно  и  при  работе  с  объектами  класса  String:  поддерживается,  к  примеру,

String ver, noun;

следующая конструкция:

verb = noun = "count";

При первом присваивании из этой цепочки вызывается определенный ранее оператор для const char*. Тип полученного результата должен быть таким, чтобы его можно было использовать как аргумент для копирующего оператора присваивания класса String. Поэтому, хотя параметр данного оператора имеет тип const char *, возвращается все же ссылка на String.

Операторы присваивания бывают перегруженными.  Например, в нашем классе String

есть такой набор:

 

// набор перегруженных операторов присваивания

String& operator=( const String & );

String& operator=( const char * );

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

15.4. Оператор взятия индекса

Оператор взятия индекса operator[]() можно определять для классов, представляющих абстракцию контейнера, из которого извлекаются отдельные элементы. Примерами таких контейнеров могут служить наш класс String, класс IntArray, представленный в главе

2, или шаблон класса vector, определенный в стандартной библиотеке C++. Оператор

взятия индекса обязан быть функцией-членом класса.

У  пользователей  String должна  иметься  возможность  чтения  и  записи  отдельных символов члена _string. Мы хотим поддержать следующий способ применения объектов

String entry( "extravagant" ); String mycopy;

for ( int ix = 0; ix < entry.size(); ++ix )

данного класса:

mycopy[ ix ] = entry[ ix ];

Оператор  взятия  индекса  может  появляться  как  слева,  так  и  справа  от  оператора присваивания.    Чтобы    быть   в   левой    части,   он   должен    возвращать    l-значение

#include <cassert>

inine char&

String::operator[]( int elem ) const

{

assert( elem >= 0 && elem < _size );

return _string[ elem ];

индексируемого элемента. Для этого мы возвращаем ссылку:

}

String color( "violet" );

В следующем фрагменте нулевому элементу массива color присваивается символ 'V':

color[ 0 ] = 'V';

Обратите внимание, что в определении оператора проверяется выход индекса за границы массива. Для этого используется библиотечная C-функция assert(). Можно также возбудить исключение, показывающее, что значение elem меньше 0 или больше длины C-

 

строки, на которую ссылается _string. (Возбуждение и обработка исключений обсуждались в главе 11.)

15.5. Оператор вызова функции

Оператор вызова функции может быть перегружен для объектов типа класса. (Мы уже видели, как он используется, при рассмотрении объектов-функций в разделе 12.3.) Если определен класс, представляющий некоторую операцию, то для ее вызова перегружается соответствующий оператор. Например, для взятия абсолютного значения числа типа int

class absInt {

public:

int operator()( int val ) {

int result = val < 0 ? -val : val;

return result;

}

можно определить класс absInt:

};

Перегруженный   оператор   operator() должен   быть   объявлен   как  функция-член   с произвольным числом параметров. Параметры и возвращаемое значение могут иметь любые  типы,  допустимые  для  функций  (см.  разделы  7.2,  7.3  и  7.4).  operator() вызывается путем применения списка аргументов к объекту того класса, в котором он определен. Мы рассмотрим, как он используется в одном из обобщенных алгоритмов, описанных в главе 12. В следующем примере обобщенный алгоритм transform() вызывается  для  применения  определенной  в  absInt операции  к  каждому  элементу

#include <vector>

#include <algoritm>

int main() {

int ia[] = { -0, 1, -1, -2, 3, 5, -5, 8 };

vector< int > ivec( ia, ia+8 );

// заменить каждый элемент его абсолютным значением

transform( ivec.begin(), ivec.end(), ivec.begin(), absInt() );

// ...

вектора ivec, т.е. для замены элемента его абсолютным значением.

}

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

Четвертый  аргумент –  это временный  объект  класса  absInt,  создаваемый  с помощью конструктора по умолчанию. Конкретизация обобщенного алгоритма transform(), вызываемого из main(), могла бы выглядеть так:

 

typedef vector< int >::iterator iter_type;

// конкретизация transform()

// операция absInt применяется к элементу вектора int

iter_type transform( iter_type iter, iter_type last, iter_type result, absInt func )

{

while ( iter != last )

*result++ = func( *iter++ ); // вызывается absInt::operator()

return iter;

}

func – это объект класса, который предоставляет операцию absInt, заменяющую число типа  int его  абсолютным  значением.  Он  используется  для  вызова  перегруженного оператора  operator() класса  absInt.  Этому  оператору  передается  аргумент  *iter, указывающий на тот элемент вектора, для которого мы хотим получить абсолютное значение.

15.6. Оператор “стрелка”

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

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

class ScreenPtr {

// ...

private:

Screen *ptr;

Screen (см. главу 13):

};

Определение ScreenPtr должно быть таким, чтобы объект этого класса гарантировано указывал  на  объект  Screen:  в  отличие  от  встроенного  указателя,  он  не  может  быть нулевым.   Тогда   приложение   сможет   пользоваться   объектами   типа   ScreenPtr,   не проверяя,   указывают   ли   они   на   какой-нибудь   объект   Screen.   Для   этого   нужно определить  класс  ScreenPtr с  конструктором,  но  без  конструктора  по  умолчанию

class ScreenPtr {

public:

ScreenPtr( const Screen &s ) : ptr( &s ) { }

// ...

(детально конструкторы рассматривались в разделе 14.2):

};

 

В         любом             определении объекта           класса             ScreenPtr         должен           присутствовать

ScreenPtr p1;   // ошибка: у класса ScreenPtr нет конструктора по умолчанию

Screen myScreen( 4, 4 );

инициализатор – объект класса Screen, на который будет ссылаться объект ScreenPtr:

ScreenPtr ps( myScreen ); // правильно

Чтобы  класс  ScreenPtr вел  себя  как  встроенный  указатель,  необходимо  определить некоторые  перегруженные  операторы –  разыменования  (*)  и  “стрелку”  для  доступа  к

// перегруженные операторы для поддержки поведения указателя

class ScreenPtr {

public:

Screen& operator*()  { return *ptr; }

Screen* operator->() { return ptr; }

// ...

членам:

};

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

point->action();

исследуется тип point. Если это указатель на некоторый тип класса, то применяется семантика  встроенного оператора  доступа к члену. Если же это объект или ссылка  на объект, то проверяется, есть ли в этом классе перегруженный оператор доступа. Когда перегруженный оператор “стрелка” определен, он вызывается для объекта point, иначе инструкция неверна, поскольку для обращения к членам самого объекта (в том числе по ссылке) следует использовать оператор “точка”.

Перегруженный  оператор  “стрелка” должен  возвращать  либо указатель  на тип  класса, либо объект класса, в котором он определен. Если возвращается указатель, то к нему применяется семантика встроенного оператора “стрелка”. В противном случае процесс продолжается рекурсивно, пока не будет получен указатель или определена ошибка. Например,  так  можно  воспользоваться  объектом  ps класса  ScreenPtr для  доступа  к членам Screen:

ps->move( 2, 3 );

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

Ниже  приводится  небольшая  программа  для  тестирования  класса  ScreenPtr.  Объект типа ScreenPtr используется точно так же, как любой объект типа Screen*:

 

#include <iostream>

#include <string>

#include "Screen.h"

void printScreen( const ScreenPtr &ps )

{

cout << "Screen Object ( "

<< ps->height() << ", "

<< ps->width() << " ) ";

for ( int ix = 1; ix <= ps->height(); ++ix )

{

for ( int iy = 1; iy <= ps->width(); ++iy )

cout << ps->get( ix, iy );

cout << " ";

}

}

int main() {

Screen sobj( 2, 5 );

string init( "HelloWorld" ); ScreenPtr ps( sobj );

// Установить содержимое экрана

string::size_type initpos = 0;

for ( int ix = 1; ix <= ps->height(); ++ix )

for ( int iy = 1; iy <= ps->width(); ++iy )

{

ps->move( ix, iy );

ps->set( init[ initpos++ ] );

}

// Вывести содержимое экрана

printScreen( ps );

return 0;

}

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

15.7. Операторы инкремента и декремента

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

Сначала мы определим  новый член size, который содержит либо нуль (это говорит о том,  что объект ScreenPtr указывает  на единственный  объект), либо размер массива, адресуемого объектом ScreenPtr. Нам также понадобится член offset, запоминающий смещение от начала данного массива:

 

class ScreenPtr {

public:

// ... private:

int size;            // размер массива: 0, если единственный объект

int offset;        // смещение ptr от начала массива

Screen *ptr;

};

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

class ScreenPtr {

public:

 

 

private:

ScreenPtr( Screen &s , int arraySize = 0 )

: ptr( &s ), size ( arraySize ), offset( 0 ) { }

 

int size; int offset; Screen *ptr;

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

};

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

Screen myScreen( 4, 4 );

ScreenPtr pobj( myScreen ); // правильно: указывает на один объект

const int arrSize = 10;

Screen *parray = new Screen[ arrSize ];

нового класса ScreenPtr можно определять следующим образом:

ScreenPtr parr( *parray, arrSize ); // правильно: указывает на массив

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

class ScreenPtr {

public:

Screen& operator++(); Screen& operator--();

// ...

ничего неожиданного:

};

Такие   операторы   определяются   как   унарные   операторные   функции.   Использовать префиксный оператор инкремента можно, к примеру, следующим образом:

 

const int arrSize = 10;

Screen *parray = new Screen[ arrSize ];

ScreenPtr parr( *parray, arrSize );

for ( int ix = 0;

ix < arrSize;

++ix, ++parr ) // эквивалентно parr.operator++()

}

printScreen( parr );

Screen& ScreenPtr::operator++()

{

if ( size == 0 ) {

cerr << "не могу инкрементировать указатель для одного объекта ";

return *ptr;

}

if ( offset >= size - 1 ) {

cerr << "уже в конце массива ";

return *ptr;

}

++offset;

return *++ptr;

}

Screen& ScreenPtr::operator--()

{

if ( size == 0 ) {

cerr << "не могу декрементировать указатель для одного объекта ";

return *ptr;

}

if ( offset <= 0 ) {

cerr << "уже в начале массива ";

return *ptr;

}

--offset;

return *--ptr;

Определения этих перегруженных операторов приведены ниже:

}

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

class ScreenPtr {

public:

Screen& operator++();            // префиксные операторы

Screen& operator--();

Screen& operator++(int); // постфиксные операторы

Screen& operator--(int);

// ...

ScreenPtr:

};

 

Screen& ScreenPtr::operator++(int)

{

if ( size == 0 ) {

cerr << "не могу инкрементировать указатель для одного объекта ";

return *ptr;

}

if ( offset == size ) {

cerr << "уже на один элемент дальше конца массива ";

return *ptr;

}

++offset;

return *ptr++;

}

Screen& ScreenPtr::operator--(int)

{

if ( size == 0 ) {

cerr << "не могу декрементировать указатель для одного объекта ";

return *ptr;

}

if ( offset == -1 ) {

cerr << "уже на один элемент раньше начала массива ";

return *ptr;

}

--offset;

return *ptr--;

Ниже приведена возможная реализация постфиксных операторов:

}

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

const int arrSize = 10;

Screen *parray = new Screen[ arrSize ];

ScreenPtr parr( *parray, arrSize );

for ( int ix = 0; ix < arrSize; ++ix)

использования постфиксного оператора:

printScreen( parr++ );

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

parr.operator++(1024);            // вызов постфиксного operator++

Перегруженные операторы инкремента и декремента разрешается объявлять как дружественные функции. Изменим соответствующим образом определение класса ScreenPtr:

 

class ScreenPtr {

// объявления не членов

friend Screen& operator++( Screen & );         // префиксные операторы

friend Screen& operator--( Screen & );

friend Screen& operator++( Screen &, int); // постфиксные операторы

friend Screen& operator--( Screen &, int);

public:

// определения членов

};

Упражнение 15.7

Напишите определения перегруженных операторов инкремента и декремента для класса

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

Упражнение 15.8

С помощью ScreenPtr можно представить указатель на массив объектов класса Screen. Модифицируйте  перегруженные  operator*() и  operator->() (см.  раздел  15.6)  так, чтобы  указатель  ни  при  каком  условии  не  адресовал  элемент  перед  началом  или  за концом массива. Совет: в этих операторах следует воспользоваться новыми членами size и offset.

15.8. Операторы new и delete

По умолчанию выделение объекта класса из хипа и освобождение занятой им памяти выполняются  с  помощью  глобальных  операторов  new() и  delete(),  определенных  в стандартной библиотеке C++. (Мы рассматривали эти операторы в разделе 8.4.) Но класс может реализовать и собственную стратегию управления памятью, предоставив одноименные операторы-члены. Если они определены в классе, то вызываются вместо глобальных операторов с целью выделения и освобождения памяти для объектов этого класса.

Определим операторы new() и delete() в нашем классе Screen.

Оператор-член new() должен возвращать значение типа void* и принимать в качестве первого  параметра  значение типа  size_t,  где size_t – это typedef,  определенный  в

class Screen {

public:

void *operator new( size_t );

// ...

системном заголовочном файле <cstddef>. Вот его объявление:

};

Когда для создания объекта типа класса используется new(), компилятор проверяет, определен ли в этом классе такой оператор. Если да, то для выделения памяти под объект вызывается  именно  он,  в  противном  случае –  глобальный  оператор  new().  Например, следующая инструкция

Screen *ps = new Screen;

 

создает  объект  Screen в  хипе,  а  поскольку  в  этом  классе  есть  оператор  new(),  то вызывается он. Параметр size_t оператора автоматически инициализируется значением, равным размеру Screen в байтах.

Добавление  оператора   new() в  класс  или  его  удаление  оттуда  не  отражаются  на пользовательском  коде. Вызов new выглядит одинаково как для глобального оператора, так и для оператора-члена.  Если  бы  в классе Screen не было собственного new(), то обращение осталось бы правильным, только вместо оператора-члена вызывался бы глобальный оператор.

С помощью оператора разрешения глобальной области видимости можно вызвать глобальный new(), даже если в классе Screen определена собственная версия:

Screen *ps = ::new Screen;

Оператор delete(), являющийся членом класса, должен иметь тип void, а в качестве первого параметра принимать void*. Вот как выглядит его объявление

class Screen {

public:

void operator delete( void * );

для Screen:

};

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

delete ps;

освобождает   память,   занятую  объектом   класса  Screen,   на  который  указывает  ps. Поскольку в Screen есть оператор-член delete(), то применяется именно он. Параметр оператора типа void* автоматически инициализируется значением ps.

Добавление  delete() в  класс  или  его  удаление  оттуда  никак  не  сказываются  на пользовательском   коде.   Вызов   delete выглядит   одинаково   как   для   глобального оператора, так и для оператора-члена.  Если бы в классе Screen не было собственного оператора delete(), то обращение осталось бы правильным, только вместо оператора- члена вызывался бы глобальный оператор.

С помощью оператора разрешения глобальной области видимости можно вызвать глобальный  delete(), даже если в Screen определена собственная версия:

::delete ps;

В   общем   случае   используемый   оператор   delete() должен   соответствовать   тому оператору  new(),  с  помощью  которого  была  выделена  память.  Например,  если  ps указывает на область памяти, выделенную глобальным new(), то для ее освобождения следует использовать глобальный же delete().

 

Оператор  delete(),  определенный  для  типа  класса,  может  содержать  два  параметра вместо  одного.  Первый  параметр  по-прежнему  должен  иметь  тип  void*,  а  второй –

class Screen {

public:

// заменяет

// void operator delete( void * );

void operator delete( void *, size_t );

предопределенный тип size_t (не забудьте включить заголовочный файл <cstddef>):

};

Если второй параметр есть, компилятор автоматически инициализирует его значением, равным  размеру адресованного  первым  параметром  объекта  в байтах.  (Этот  параметр важен в иерархии классов, когда оператор delete() может наследоваться производным классом. Подробнее наследование обсуждается в главе 17.)

Рассмотрим реализацию операторов new() и delete() в классе Screen более детально. В основе нашей стратегии распределения памяти будет лежать связанный список объектов Screen, на начало которого указывает член freeStore. При каждом обращении к оператору-члену new() возвращается следующий объект из списка. При вызове delete() объект возвращается в список. Если при создании нового объекта список, адресованный freeStore,  пуст,  то  вызывается  глобальный  оператор  new(),  чтобы  получить  блок памяти, достаточный для хранения screenChunk объектов класса Screen.

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

class Screen {

public:

void *operator new( size_t );

void operator delete( void *, size_t );

// ...

private:

Screen *next;

static Screen *freeStore;

static const int screenChunk;

Screen, нам понадобится третий член next:

};

Вот одна из возможных реализаций оператора new() для класса Screen:

 

#include "Screen.h"

#include <cstddef>

// статические члены инициализируются

// в исходных файлах программы, а не в заголовочных файлах

Screen *Screen::freeStore = 0;

const int Screen::screenChunk = 24;

void *Screen::operator new( size_t size )

{

Screen *p;

if ( !freeStore ) {

// связанный список пуст: получить новый блок

// вызывается глобальный оператор new size_t chunk = screenChunk * size;

freeStore = p =

reinterpret_cast< Screen* >( new char[ chunk ] );

// включить полученный блок в список

for ( ;

p != &freeStore[ screenChunk - 1 ];

++p )

p->next = p+1;

p->next = 0;

}

p = freeStore;

freeStore = freeStore->next;

return p;

}

void Screen::operator delete( void *p, size_t )

{

// вставить "удаленный" объект назад,

// в список свободных

( static_cast< Screen* >( p ) )->next = freeStore;

freeStore = static_cast< Screen* >( p );

А вот реализация оператора delete():

}

Оператор new() можно объявить в классе и без соответствующего  delete().  В таком случае объекты освобождаются с помощью одноименного глобального оператора. Разрешается также объявить и оператор delete() без new(): объекты будут создаваться с помощью одноименного глобального оператора. Однако обычно эти операторы реализуются одновременно, как в примере выше, поскольку разработчику класса, как правило, нужны оба.

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

13.5.) Причина,  по которой эти операторы делаются статическими,  заключается в том,

 

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

Выделение памяти с помощью оператора new(), например:

Screen *ptr = new Screen( 10, 20 );

// Псевдокод на C++

ptr = Screen::operator new( sizeof( Screen ) );

эквивалентно последовательному выполнению таких инструкций:

Screen::Screen( ptr, 10, 20 );

Иными словами, сначала вызывается определенный в классе оператор new(), чтобы выделить  память  для  объекта,  а  затем  этот  объект  инициализируется  конструктором. Если new() неудачно завершает работу, то возбуждается исключение типа bad_alloc и конструктор не вызывается.

Освобождение памяти с помощью оператора delete(), например:

delete ptr;

// Псевдокод на C++ Screen::~Screen( ptr );

эквивалентно последовательному выполнению таких инструкций:

Screen::operator delete( ptr, sizeof( *ptr ) );

Таким образом, при уничтожении объекта сначала вызывается деструктор класса, а затем определенный в классе оператор delete() для освобождения памяти. Если значение ptr равно 0, то ни деструктор, ни delete() не вызываются.

15.8.1. Операторы new[ ] и delete [ ]

Оператор new(), определенный в предыдущем подразделе, вызывается только при выделении памяти для единичного объекта. Так, в данной инструкции вызывается new()

// вызывается Screen::operator new()

класса Screen:

Screen *ps = new Screen( 24, 80 );

тогда как ниже вызывается глобальный оператор new[]() для выделения из хипа памяти

// вызывается Screen::operator new[]()

под массив объектов типа Screen:

 

Screen *psa = new Screen[10];

В  классе  можно  объявить  также  операторы  new[]() и  delete[]() для  работы  с массивами.

Оператор-член new[]() должен возвращать значение типа void* и принимать в качестве

class Screen {

public:

void *operator new[]( size_t );

// ...

первого параметра значение типа size_t. Вот его объявление для Screen:

};

Когда  с помощью  new создается  массив объектов  типа  класса, компилятор  проверяет, определен ли в классе оператор new[](). Если да, то для выделения памяти под массив вызывается именно он, в противном случае – глобальный new[](). В следующей инструкции в хипе создается массив из десяти объектов Screen:

Screen *ps = new Screen[10];

В этом классе есть оператор new[](), поэтому он и вызывается для выделения памяти. Его  параметр   size_t автоматически   инициализируется   значением,   равным  объему памяти в байтах, необходимому для размещения десяти объектов Screen.

Даже если в классе имеется оператор-член new[](), программист может вызвать для создания массива глобальный new[](), воспользовавшись оператором разрешения глобальной области видимости:

Screen *ps = ::new Screen[10];

Оператор delete(), являющийся членом класса, должен иметь тип void, а в качестве

class Screen {

public:

void operator delete[]( void * );

первого параметра принимать void*. Вот как выглядит его объявление для Screen:

};

Чтобы удалить массив объектов класса, delete должен вызываться следующим образом:

delete[] ps;

Когда   операндом   delete является   указатель   на   объект   типа   класса,   компилятор проверяет,   определен   ли   в   этом   классе   оператор   delete[]().   Если   да,   то   для освобождения  памяти  вызывается  именно  он,  в  противном  случае  –  его  глобальная версия. Параметр типа void* автоматически инициализируется значением адреса начала области памяти, в которой размещен массив.

 

Даже если в классе имеется оператор-член delete[](), программист может вызвать глобальный delete[](), воспользовавшись оператором разрешения глобальной области видимости:

::delete[] ps;

Добавление  операторов  new[]() или  delete[]() в  класс  или  удаление их  оттуда  не отражаются на пользовательском коде: вызовы как глобальных операторов, так и операторов-членов выглядят одинаково.

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

При уничтожении массива сначала вызывается деструктор класса для уничтожения элементов,  а  затем  оператор  delete[]() –  для  освобождения  всей  памяти.  При  этом важно использовать правильный синтаксис. Если в инструкции

delete ps;

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

У оператора-члена  delete[]() может быть не один, а два параметра, при этом второй

class Screen {

public:

// заменяет

// void operator delete[]( void* );

void operator delete[]( void*, size_t );

должен иметь тип size_t:

};

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

15.8.2. Оператор размещения new() и оператор delete()

Оператор-член  new() может быть перегружен  при условии, что все объявления имеют

class Screen {

public:

void *operator new( size_t );

void *operator new( size_t, Screen * );

// ...

разные списки параметров. Первый параметр должен иметь тип size_t:

 

};

Остальные  параметры  инициализируются   аргументами  размещения,   заданными  при

void func( Screen *start ) {

Screen *ps = new (start) Screen;

// ...

вызове new:

}

Та  часть  выражения,  которая  находится  после  ключевого  слова  new и  заключена  в круглые скобки, представляет аргументы размещения. В примере выше вызывается оператор new(), принимающий два параметра. Первый автоматически инициализируется значением,  равным  размеру  класса  Screen в  байтах,  а  второй –  значением  аргумента размещения start.

Можно также перегружать и оператор-член delete(). Однако такой оператор никогда не вызывается   из   выражения   delete.   Перегруженный   delete() неявно   вызывается компилятором,  если  конструктор,  вызванный  при  выполнении  оператора  new (это не опечатка, мы действительно имеем в виду new), возбуждает исключение. Рассмотрим использование delete() более внимательно.

Последовательность действий при вычислении выражения

Screen *ps = new ( start ) Screen;

такова:

1.   Вызывается определенный в классе оператор new(size_t, Screen*).

2.   Вызывается    конструктор    по   умолчанию    класса    Screen для   инициализации созданного объекта.

Переменная ps инициализируется адресом нового объекта Screen.

Предположим, что оператор класса new(size_t, Screen*) выделяет память с помощью глобального  new().  Как  разработчик  может  гарантировать,  что  память  будет освобождена, если вызванный на шаге 2 конструктор возбуждает исключение? Чтобы защитить пользовательский код от утечки памяти, следует предоставить перегруженный оператор delete(), который вызывается только в подобной ситуации.

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

Screen *ps = new (start) Screen;

Если конструктор по умолчанию класса Screen возбуждает исключение, то компилятор ищет delete() в области видимости Screen. Чтобы такой оператор был найден, типы его параметров должны соответствовать типам параметров вызванного new(). Поскольку первый  параметр  new() всегда  имеет  тип  size_t,  а  оператора  delete() – void*,  то первые параметры при сравнении не учитываются. Компилятор ищет в классе Screen оператор delete() следующего вида:

 

void operator delete( void*, Screen* );

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

когда new() возбуждает исключение. (Иначе – не вызывается.)

Разработчик класса принимает решение, предоставлять ли delete(), соответствующий некоторому  new(),  в  зависимости  от  того,  выделяет  ли  этот  оператор  new() память самостоятельно или пользуется уже выделенной. В первом случае delete() необходимо включить для освобождения памяти, если конструктор возбудит исключение; иначе в нем нет необходимости.

Можно  также  перегрузить  оператор  размещения  new[]() и  оператор  delete[]() для

class Screen {

public:

void *operator new[]( size_t );

void *operator new[]( size_t, Screen* ); void operator delete[]( void*, size_t ); void operator delete[]( void*, Screen* );

// ...

массивов:

};

Оператор  new[]() используется  в  случае,  когда  в  выражении,  содержащем  new для

void func( Screen *start ) {

// вызывается Screen::operator new[]( size_t, Screen* )

Screen *ps = new (start) Screen[10];

// ...

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

}

Если при работе оператора new конструктор возбуждает исключение, то автоматически вызывается соответствующий delete[]().

Упражнение 15.9

class iStack {

public:

iStack( int capacity )

: _stack( capacity ), _top( 0 ) {}

// ... private:

int _top;

vatcor< int > _stack;

Объясните, какие из приведенных инициализаций ошибочны:

};

 

(a) iStack *ps = new iStack(20);

(b) iStack *ps2 = new const iStack(15);

(c) iStack *ps3 = new iStack[ 100 ];

Упражнение 15.10

class Exercise {

public: Exercise();

~Exercise();

};

Exercise *pe = new Exercise[20];

Что происходит в следующих выражениях, содержащих new и delete?

delete[] ps;

Измените   эти   выражения   так,   чтобы   вызывались   глобальные  операторы   new() и

delete().

Упражнение 15.11

Объясните, зачем разработчик класса должен предоставлять оператор delete().

15.9. Определенные пользователем преобразования

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

char ch; short sh;, int ival;

/* в каждой операции один операнд

* требует преобразования типа */

ch + ival;         ival + ch;

ch + sh;            ch + ch;

следующие шесть операций сложения:

ival + sh;          sh + ival;

Операнды ch и sh расширяются до типа int. При выполнении операции складываются два значения типа int. Расширение типа неявно выполняется компилятором и для пользователя прозрачно.

В этом разделе мы рассмотрим, как разработчик может определить собственные преобразования для объектов типа класса. Такие определенные пользователем преобразования также автоматически вызываются компилятором по мере необходимости. Чтобы показать, зачем они нужны, обратимся снова к классу SmallInt, введенному в разделе 10.9.

 

Напомним, что SmallInt позволяет определять объекты, способные хранить значения из того же диапазона, что unsigned char, т.е. от 0 до 255, и перехватывает ошибки выхода за его границы. Во всех остальных отношениях этот класс ведет себя точно так же, как unsigned char.

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

class SmallInt {

friend operator+( const SmallInt &, int ); friend operator-( const SmallInt &, int ); friend operator-( int, const SmallInt & ); friend operator+( int, const SmallInt & );

public:

SmallInt( int ival ) : value( ival ) { }

operator+( const SmallInt & );

operator-( const SmallInt & );

// ... private:

int value;

операторных функций:

};

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

SmallInt si( 3 );

приведен к типу int. Например, выражение

si + 3.14159

разрешается в два шага:

1.   Константа 3.14159 типа double преобразуется в целое число 3.

2.   Вызывается operator+(const SmallInt &,int), который возвращает значение 6.

Если мы хотим поддержать битовые и логические операции, а также операции сравнения и  составные  операторы  присваивания,  то  сколько  же  необходимо  перегрузить операторов? Сразу и не сосчитаешь. Значительно удобнее автоматически преобразовать объект класса SmallInt в объект типа int.

В  языке  C++  имеется  механизм,  позволяющий  в  любом  классе  задать  набор преобразований, применимых к его объектам. Для SmallInt мы определим приведение объекта к типу int. Вот его реализация:

 

class SmallInt {

public:

SmallInt( int ival ) : value( ival ) { }

// конвертер

// SmallInt ==> int

operator int() { return value; }

// перегруженные операторы не нужны

private:

int value;

};

Оператор     int() –    это    конвертер,     реализующий     определенное     пользователем преобразование,    в данном случае приведение типа класса к заданному типу int. Определение конвертера описывает, что означает преобразование и какие действия компилятор  должен  выполнить  для  его  применения.  Для  объекта  SmallInt смысл преобразования в int заключается в том, чтобы вернуть число типа int, хранящееся в члене value.

Теперь  объект  класса  SmallInt можно  использовать  всюду,  где  допустимо использование int. Если предположить, что перегруженных операторов больше нет и в

SmallInt si( 3 );

SmallInt определен конвертер в int, операция сложения

si + 3.14159

разрешается двумя шагами:

1.   Вызывается конвертер класса SmallInt, который возвращает целое число 3.

2.   Целое число 3 расширяется до 3.0 и складывается  с константой двойной  точности

3.14159, что дает 6.14159.

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

В этой программе иллюстрируется применение класса SmallInt:

 

#include <iostream>

#include "SmallInt.h"

int main() {

cout << "Введите SmallInt, пожалуйста: ";

while ( cin >> si1 ) {

cout << "Прочитано значение "

<< si1 << " Оно ";

// SmallInt::operator int() вызывается дважды

cout << ( ( si1 > 127 )

? "больше, чем "

: ( ( si1 < 127 )

? "меньше, чем "

: "равно ") ) << "127 ";

cout << "Введите SmallInt, пожалуйста (ctrl-d для выхода): ";

}

cout <<"До встречи ";

}

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

Введите SmallInt, пожалуйста: 127

Прочитано значение 127

Оно равно 127

Введите SmallInt, пожалуйста (ctrl-d для выхода): 126

Оно меньше, чем 127

Введите SmallInt, пожалуйста (ctrl-d для выхода): 128

Оно больше, чем 127

Введите SmallInt, пожалуйста (ctrl-d для выхода): 256

*** Ошибка диапазона SmallInt: 256 ***

#include <iostream>

class SmallInt {

friend istream&

operator>>( istream &is, SmallInt &s );

friend ostream&

operator<<( ostream &is, const SmallInt &s )

{ return os << s.value; }

public:

SmallInt( int i=0 ) : value( rangeCheck( i ) ){}

int operator=( int i )

{ return( value = rangeCheck( i ) ); }

operator int() { return value; }

private:

int rangeCheck( int );

int value;

В реализацию класса SmallInt добавили поддержку новой функциональности:

};

 

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

int ix;

is >> ix;

si = ix;             // SmallInt::operator=(int)

return is;

}

int SmallInt::rangeCheck( int i )

{

/* если установлен хотя бы один бит, кроме первых восьми,

* то значение слишком велико; сообщить и сразу выйти */

if ( i & ~0377 ) {

cerr << " *** Ошибка диапазона SmallInt: "

<< i << " ***" << endl;

exit( -1 );

}

return i;

Ниже приведены определения функций-членов, находящиеся вне тела класса:

}

15.9.1. Конвертеры

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

Имя, находящееся за ключевым словом, не обязательно должно быть именем одного из встроенных типов. В показанном ниже классе Token определено несколько конвертеров. В одном из них для задания имени типа используется typedef tName, а в другом – тип

#include "SmallInt.h"

typedef char *tName;

class Token {

public:

Token( char *, int );

operator SmallInt() { return val; }

operator tName()         { return name; }

operator int()   { return val; }

// другие открытые члены

private:

SmallInt val;

char *name;

класса SmallInt.

};

Обратите внимание,  что определения конвертеров  в типы SmallInt и int одинаковы. Конвертер  Token::operator int() возвращает  значение  члена  val.  Поскольку  val имеет   тип   SmallInt,   то   неявно   применяется   SmallInt::operator  int() для преобразования  val в  тип  int.  Сам  Token::operator int() неявно  употребляется компилятором для преобразования объекта типа Token в значение типа int. Например,

 

этот конвертер используется для неявного приведения фактических аргументов t1 и t2

#include "Token.h"

void print( int i )

{

cout << "print( int ) : " << i << endl;

}

Token t1( "integer constant", 127 ); Token t2( "friend", 255 );

int main()

{

print( t1 );   // t1.operator int() print( t2 );   // t2.operator int() return 0;

типа Token к типу int формального параметра функции print():

}

print( int ) : 127

После компиляции и запуска программа выведет такие строки:

print( int ) : 255

Общий вид конвертера следующий:

operator type();

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

operator int( SmallInt & ); // ошибка: не член

class SmallInt {

public:

int operator int();         // ошибка: задан тип возвращаемого значения

operator int( int = 0 ); // ошибка: задан список параметров

// ...

значения, ни список параметров:

};

Конвертер вызывается в результате явного преобразования типов. Если преобразуемое значение имеет тип класса, у которого есть конвертер, и в операции приведения указан тип этого конвертера, то он и вызывается:

 

#include "Token.h"

Token tok( "function", 78 );

// функциональная нотация: вызывается Token::operator SmallInt() SmallInt tokVal = SmallInt( tok );

// static_cast: вызывается Token::operator tName()

char *tokName = static_cast< char * >( tok );

У конвертера Token::operator tName() может быть нежелательный побочный эффект. Попытка прямого обращения к закрытому члену Token::name помечается компилятором как ошибка:

char *tokName = tok.name;  // ошибка: Token::name - закрытый член

Однако наш конвертер, разрешая пользователям непосредственно изменять Token::name,

делает  как  раз  то,  от чего мы  хотели  защититься.  Скорее  всего,  это не годится.  Вот,

#include "Token.h"

Token tok( "function", 78 );

char *tokName = tok;             // правильно: неявное преобразование

например, как могла бы произойти такая модификация:

*tokname = 'P';  // но теперь в члене name находится Punction!

Мы намереваемся  разрешить  доступ  к преобразованному  объекту класса Token только

typedef const char *cchar;

class Token {

public:

operator cchar() { return name; }

// ...

};

// ошибка: преобразование char* в const char* не допускается

char *pn = tok;

для чтения. Следовательно, конвертер должен возвращать тип const char*:

const char *pn2 = tok;            // правильно

Другое   решение –   заменить   в   определении   Token тип   char* на   тип   string из стандартной библиотеки C++:

 

class Token {

public:

Token( string, int );

operator SmallInt() { return val; }

operator string()          { return name; }

operator int()   { return val; }

// другие открытые члены

private:

SmallInt val;

string name;

};

Семантика конвертера Token::operator string() состоит в возврате копии значения (а не указателя на значение) строки, представляющей имя лексемы. Это предотвращает случайную модификацию закрытого члена name класса Token.

Должен ли целевой тип точно соответствовать типу конвертера? Например, будет ли в

extern void calc( double ); Token tok( "constant", 44 );

// Вызывается ли оператор int()? Да

// применяется стандартное преобразование int --> double

следующем коде вызван конвертер int(), определенный в классе Token?

calc( tok );

Если целевой тип (в данном случае double) не точно соответствует типу конвертера (в нашем случае int), то конвертер все равно будет вызван при условии, что существует последовательность стандартных преобразований, приводящая к целевому типу из типа конвертера. (Эти последовательности описаны в разделе 9.3.) При обращении к функции calc() вызывается Token::operator int() для преобразования  tok из типа Token в тип  int.  Затем  для  приведения  результата  от  типа  int к  типу  double применяется стандартное преобразование.

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

extern void calc( int ); Token tok( "pointer", 37 );

// если Token::operator int() не определен,

// то этот вызов приводит к ошибке компиляции

ошибочным:

calc( tok );

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

 

Token::operator SmallInt()

а затем результат привести к типу int – тоже с помощью пользовательского конвертера

Token::operator int()

Вызов calc(tok) помечается компилятором как ошибка, так как не существует неявного преобразования из типа Token в тип int.

Если логического соответствия между типом конвертера и типом класса нет, назначение

class Date {

public:

// попробуйте догадаться, какой именно член возвращается!

operator int();

private:

int month, day, year;

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

};

Какое значение должен вернуть конвертер int() класса Date? Сколь бы основательными ни были причины для того или иного решения, читатель останется в недоумении относительно того, как пользоваться объектами класса Date, поскольку между ними и целыми числами нет явного логического соответствия. В таких случаях лучше вообще не определять конвертер.

15.9.2. Конструктор как конвертер

Набор конструкторов класса, принимающих единственный параметр, например, SmallInt(int) класса  SmallInt,  определяет  множество  неявных  преобразований    в значения типа SmallInt. Так,   конструктор SmallInt(int) преобразует значения типа

extern void calc( SmallInt );

int i;

// необходимо преобразовать i в значение типа SmallInt

// это достигается применением SmallInt(int)

int в значения типа SmallInt.

calc( i );

При  вызове  calc(i) число  i преобразуется  в  значение  типа  SmallInt с  помощью конструктора   SmallInt(int),   вызванного   компилятором   для   создания   временного объекта нужного типа. Затем копия этого объекта передается в calc(), как если бы вызов функции был записан в форме:

 

// Псевдокод на C++

// создается временный объект типа SmallInt

{

SmallInt temp = SmallInt( i );

calc( temp );

}

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

class Number {

public:

// создание значения типа Number из значения типа SmallInt

Number( const SmallInt & );

// ...

Типом параметра конструктора может быть тип некоторого класса:

};

В  таком  случае  значение  типа  SmallInt можно  использовать  всюду,  где  допустимо

extern void func( Number ); SmallInt si(87);

int main()

{ // вызывается Number( const SmallInt & )

func( si );

// ...

значение типа Number:

}

Если конструктор используется для выполнения неявного преобразования, то должен ли тип его параметра точно соответствовать типу подлежащего преобразованию значения? Например, будет ли в следующем коде вызван SmallInt(int), определенный в классе

extern void calc( SmallInt );

double dobj;

// вызывается ли SmallInt(int)? Да

// dobj преобразуется приводится от double к int

// стандартным преобразованием

SmallInt, для приведения dobj к типу SmallInt?

calc( dobj );

Если  необходимо,  к  фактическому  аргументу  применяется  последовательность стандартных преобразований до того, как вызвать конструктор, выполняющий определенное пользователем преобразование. При обращении к функции calc()употребляется стандартное преобразование dobj из типа double в тип int. Затем уже для приведения результата к типу SmallInt вызывается SmallInt(int).

 

Компилятор неявно использует конструктор с единственным параметром для преобразования его типа в тип класса, к которому принадлежит конструктор. Однако иногда удобнее,  чтобы конструктор  Number(const SmallInt&) можно было вызывать только для инициализации объекта типа Number значением типа SmallInt, но ни в коем случае    не    для    выполнения    неявных    преобразований.    Чтобы    избежать    такого

class Number {

public:

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

explicit Number( const SmallInt & );

// ...

употребления конструктора, объявим его явным (explicit):

};

Компилятор   никогда   не  применяет   явные   конструкторы   для  выполнения   неявных

extern void func( Number ); SmallInt si(87);

int main()

{ // ошибка: не существует неявного преобразования из SmallInt в Number func( si );

// ...

преобразований типов:

}

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

SmallInt si(87);

int main()

{ // ошибка: не существует неявного преобразования из SmallInt в Number

func( si );

func( Number( si ) ); // правильно: приведение типа

func( static_cast< Number >( si ) ); // правильно: приведение типа

оно запрошено явно в форме оператора приведения типа:

}

15.10. Выбор преобразования A

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

 

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

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

Определенное пользователем преобразование ->

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

где определенное пользователем преобразование реализуется конвертером либо конструктором.

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

В классе разрешается определять много конвертеров. Например, в нашем классе Number их  два:  operator int() и  operator float(),  причем  оба  способны  преобразовать объект  типа  Number в  значение  типа  float.  Естественно,  можно  воспользоваться конвертером  Token::operator float() для  прямой  трансформации.  Но  и Token::operator int() тоже подходит,  так как результат  его применения имеет тип int и, следовательно, может быть преобразован в тип float с помощью стандартного преобразования.  Является  ли  трансформация  неоднозначной,  если  имеется  несколько

class Number {

public:

operator float();

operator int();

// ...

};

Number num;

таких последовательностей? Или какую-то из них можно предпочесть остальным?

float ff = num;            // какой конвертер? operator float()

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

1.   operator float() -> точное соответствие

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

Как было сказано в разделе 9.3, точное соответствие лучше стандартного преобразования. Поэтому первая последовательность лучше второй, а значит, выбирается конвертер Token::operator float().

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

 

class SmallInt {

public:

SmallInt( int ival ) : value( ival ) { } SmallInt( double dval )

: value( static_cast< int >( dval ) );

{ }

};

extern void manip( const SmallInt & );

int main() {

double dobj;

manip( dobj );  // правильно: SmallInt( double )

}

Здесь    в    классе    SmallInt определено    два    конструктора –    SmallInt(int) и SmallInt(double), которые можно использовать для изменения значения типа double в объект   типа   SmallInt: SmallInt(double) трансформирует   double в   SmallInt напрямую,   а   SmallInt(int) работает   с   результатом   стандартного   преобразования double в   int.   Таким   образом,   имеются   две   последовательности   определенных пользователем преобразований:

1.   точное соответствие -> SmallInt( double )

2.   стандартное преобразование -> SmallInt( int )

Поскольку точное соответствие лучше стандартного преобразования, то выбирается конструктор SmallInt(double).

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

class Number {

public:

operator float();

operator int();

// ...

классе Number есть два конвертера:

};

то  невозможно  неявно  преобразовать  объект  типа  Number в  тип  long.  Следующая инструкция    вызывает    ошибку    компиляции,    так    как    выбор    последовательности

// ошибка: можно применить как float(), так и int()

определенных пользователем преобразований неоднозначен:

long lval = num;

Для трансформации num в значение типа long применимы две такие последовательности:

1.   operator float() -> стандартное преобразование

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

 

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

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

С помощью явного приведения типов программист способен задать нужное изменение:

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 ) ); // ошибка: по-прежнему неоднозначно

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

 

использование  конструкторов   при  выполнении   неявных   преобразований   (а   значит,

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

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 );

// ...

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

}

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

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

 

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

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 принимаются  во  внимание  и  добавляются  к  множеству  кандидатов  также друзья этого класса, объявленные в списке его членов.

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

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

 

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. Функции-кандидаты для вызова функции в области видимости класса

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

 

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) в данное множество не включается.

 

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

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).

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

 

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 с  помощью  конвертера

 

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( si );      // вызывается manip( int )

};

}

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

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

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

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

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

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

 

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

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,  определенный  в  начале этого раздела, реализован не так:

 

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 );     // какая функция?

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

}

 

15.11. Разрешение перегрузки и функции-члены A

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

1.   Отбор функций-кандидатов.

2.   Отбор устоявших функций.

3.   Выбор наилучшей из устоявших функции.

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

15.11.1. Объявления перегруженных функций-членов

class myClass {

public:

void f( double );

char f( char, char ); // перегружает myClass::f( double )

// ...

Функции-члены класса можно перегружать:

};

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

class myClass {

public:

void mf();

double mf();    // ошибка: так перегружать нельзя

// ...

компиляции:

};

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

class myClass {

public:

void mf();

void mf();        // ошибка: повторное объявление

// ...

повторное объявление:

 

};

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

Множество  перегруженных  функций-членов  может  содержать  как  статические,  так  и

class myClass {

public:

void mcf( double );

static void mcf( int* );            // перегружает myClass::mcf( double )

// ...

нестатические функции:

};

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

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

mc.mf( arg );

Рассмотрим два вида вызовов функции-члена:

pmc->mf( arg );

где mc – выражение типа myClass, а pmc – выражение типа “указатель на тип myClass”. Множество кандидатов для обоих вызовов составлено из функций, найденных в области видимости класса myClass при поиске объявления mf().

Аналогично для вызова функции вида

myClass::mf( arg );

множество  кандидатов  также  состоит  из  функций,  найденных  в  области  видимости класса myClass при поиске объявления mf(). Например:

 

class myClass {

public:

void mf( double );

void mf( char, char = ' ' );

static void mf( int* );

// ...

};

int main() { myClass mc; int iobj;

mc.mf( iobj );

}

Кандидатами  для  вызова  функции  в  main() являются  все  три  функции-члена  mf(),

void mf( double );

void mf( char, char = ' ' );

объявленные в myClass:

static void mf( int* );

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

15.11.3. Устоявшие функции

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

class myClass {

public:

void mf( double );

void mf( char, char = ' ' );

static void mf( int* );

// ...

};

int main() { myClass mc; int iobj;

mc.mf( iobj ); // какая именно функция-член mf()? Неоднозначно

Например:

}

В этом фрагменте для вызова mf() из main() есть две устоявшие функции:

 

void mf( double );

void mf( char, char = ' ' );

mf(double) устояла  потому,  что  у  нее  только  один  параметр  и  существует стандартное преобразование аргумента iobj типа int в параметр типа double;

mf(char,char) устояла потому, что для второго параметра имеется значение по умолчанию и существует стандартное преобразование аргумента iobj типа int в тип char первого формального параметра.

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

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

Независимо от вида вызова функции, в множество устоявших могут быть включены как

class myClass {

public:

static void mf( int );

char mf( char );

};

int main() {

char cobj;

myClass::mf( cobj ); // какая именно функция-член?

статические, так и нестатические члены:

}

Здесь функция-член mf() вызывается с указанием имени класса и оператора разрешения области видимости myClass::mf(). Однако не задан ни объект (с оператором “точка”), ни  указатель  на  объект  (с  оператором  “стрелка”).  Несмотря  на  это,  нестатическая функция-член   mf(char) все   же   включается   в   множество   устоявших   наряду   со статическим членом mf(int).

Затем процесс разрешения перегрузки продолжается: на основе ранжирования преобразований типов, примененных к фактическим аргументам, чтобы выбрать наилучшую  из  устоявших  функций.  Аргумент  cobj типа  char точно  соответствует формальному   параметру  mf(char) и  может  быть  расширен   до  типа  формального параметра mf(int). Поскольку ранг точного соответствия выше, то выбирается функция mf(char).

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

 

Еще одна особенность функций-членов, которую надо принимать во внимание при формировании множества устоявших функций, – это наличие спецификаторов const или volatile у  нестатических  членов.  (Они  рассматривались  в  разделе  13.3.)  Как  они влияют  на  процесс  разрешения  перегрузки?  Пусть  в  классе  myClass есть  следующие

class myClass {

public:

static void mf( int* );

void mf( double );

void mf( int ) const;

// ...

функции-члены:

};

Тогда   и  статическая   функция-член   mf(int*),   и  константная   функция  mf(int),   и неконстантная    функция    mf(double) включаются    в    множество    кандидатов    для

int main() {

const myClass mc;

double dobj;

mc.mf( dobj ); // какая из функций-членов mf()?

показанного ниже вызова. Но какие из них войдут в множество устоявших?

}

Исследуя преобразования, которые надо применить к фактическим аргументам, мы обнаруживаем, что устояли функции mf(double) и mf(int). Тип double фактического аргумента dobj точно соответствует типу формального параметра mf(double) и может быть приведен к типу параметра mf(int) с помощью стандартного преобразования.

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

mc –  это  константный  объект,  для  которого  можно  вызывать  только  нестатические константные функции-члены. Следовательно, неконстантная функция-член mf(double) исключается из множества устоявших, и остается в нем единственная функция mf(int), которая и вызывается.

А если константный  объект использован для вызова статической функции-члена? Ведь для такой функции нельзя задавать спецификатор const или volatile, так можно ли ее

class myClass {

public:

static void mf( int );

char mf( char );

};

int main() {

const myClass mc;

int iobj;

mc.mf( iobj );  // можно ли вызывать статическую функцию-член?

вызывать через константный объект?

 

}

Статические   функции-члены   являются   общими   для   всех   объектов   одного   класса. Напрямую  они  могут  обращаться  только  к  статическим  членам  класса.  Так, нестатические члены константного объекта mc недоступны статической mf(int). По этой причине разрешается вызывать статическую функцию-член для константного объекта с помощью операторов “точка” или “стрелка”.

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

В примере выше mc – константный объект, поэтому функция-член mf(char) исключается из множества  устоявших.  Но функция-член  mf(int) в нем остается,  так как является статической. Поскольку это единственная устоявшая функция, она и оказывается наилучшей.

15.12. Разрешение перегрузки и операторы A

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

SomeClass sc;

при инициализации встретился оператор сложения:

int iobj = sc + 3;

Как  компилятор  решает,  что  следует  сделать:  вызвать  перегруженный  оператор  для класса  SomeClass или  конвертировать  операнд  sc во  встроенный  тип,  а  затем  уже воспользоваться встроенным оператором?

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

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

представленная в разделе 9.2:

1.   Отбор функций-кандидатов.

2.   Отбор устоявших функций.

3.   Выбор наилучшей из устоявших функции.

Рассмотрим эти шаги более детально.

Разрешение перегрузки функции не применяется, если все операнды имеют встроенные типы.  В  таком  случае  гарантированно  употребляется  встроенный  оператор. (Использование   операторов   с   операндами   встроенных   типов   описано   в   главе   4.) Например:

 

class SmallInt {

public:

SmallInt( int );

};

SmallInt operator+ ( const SmallInt &, const SmallInt & );

void func() {

int i1, i2;

int i3 = i1 + i2;

}

Поскольку  операнды  i1 и  i2 имеют  тип  int,  а  не  тип  класса,  то  при  сложении используется  встроенный оператор +. Перегруженный operator+(const SmallInt &, const SmallInt &) игнорируется, хотя операнды можно привести к типу SmallInt с помощью определенного пользователем преобразования в виде конструктора SmallInt(int). Описанный ниже процесс разрешения перегрузки в таких ситуациях не применяется.

Кроме  того,  разрешение  перегрузки  для  операторов  употребляется  только  в  случае

void func() { SmallInt si(98); int iobj = 65;

int res = si + iobj;  // использован операторный синтаксис

использования операторного синтаксиса:

}

Если вместо этого использовать синтаксис вызова функции:

int res = operator+( si, iobj ); // синтаксис вызова функции

то применяется процедура разрешения перегрузки для функций в пространстве имен (см.

// синтаксис вызова функции-члена

раздел 15.10). Если же использован синтаксис вызова функции-члена:

int res = si.operator+( iobj );

то работает соответствующая процедура для функций-членов (см. раздел 15.11).

15.12.1. Операторные функции-кандидаты

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

SmallInt si(98);

int iobj = 65;

При использовании следующего оператора сложения

int res = si + iobj;

 

операторной  функцией-кандидатом  является  operator+.  Какие объявления  operator+

принимаются во внимание?

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

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

SmallInt operator+ ( const SmallInt &, const SmallInt & );

int main() { SmallInt si(98); int iobj = 65;

int res = si + iobj; // ::operator+() - функция-кандидат

кандидат в случае применения operator+() внутри main():

}

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

namespace NS {

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

SmallInt operator+ ( const SmallInt&, double );

}

int main() {

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

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

// NS::operator+() - функция-кандидат

int res = si + 566;

return 0;

считаются кандидатами:

}

Операнд  si имеет  тип  класса  SmallInt,  объявленного  в пространстве имен NS. Поэтому перегруженный  operator+(const SmallInt, double), объявленный  в том же пространстве, добавляется к множеству кандидатов;

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

 

namespace NS {

class SmallInt {

friend SmallInt operator+( const SmallInt&, int )

{ /* ... */ }

};

}

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

// функция-друг operator+() - кандидат

int res = si + 566;

return 0;

}

Операнд si имеет тип SmallInt. Операторная функция operator+(const SmallInt&, int),    являющаяся    другом    этого    класса, –    член    пространства    имен    NS,    хотя непосредственно в этом пространстве она не объявлена. При обычном поиске в NS эта операторная  функция  не  будет  найдена.  Однако  при  использовании  operator+() с аргументом  типа  SmallInt функции-друзья,  объявленные  в  области  видимости  этого класса, включаются в рассмотрение и добавляются к множеству кандидатов.

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

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

class myFloat {

myFloat( double );

};

class SmallInt {

public:

SmallInt( int );

SmallInt operator+ ( const myFloat & );

};

int main() { SmallInt si(15);

int res = si + 5.66; // оператор-член operator+() - кандидат

класса:

}

Оператор-член  SmallInt::operator+(const myFloat &), определенный  в SmallInt, включается  в  множество  функций-кандидатов  для  разрешения  вызова  operator+() в main();

множество     встроенных    операторов.    Учитывая       типы, которые    можно использовать со встроенным operator+(), кандидатами являются также:

 

int operator+( int, int );

double operator+( double, double );

T* operator+( T*, I ); T* operator+( I, T* );

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

Любое  из  первых  четырех  множеств  может  оказаться  пустым.  Например,  если  среди членов  класса SmallInt нет функции с именем operator+(),  то четвертое множество будет пусто.

Все    множество    операторных    функций-кандидатов    является    объединением    пяти

namespace NS {

class myFloat {

myFloat( double );

};

class SmallInt {

friend SmallInt operator+( const SmallInt &, int ) { /* ... */ }

public:

SmallInt( int );

operator int();

SmallInt operator+ ( const myFloat & );

// ...

};

SmallInt operator+ ( const SmallInt &, double );

}

int main() {

// тип si - class SmallInt:

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

int res = si + 5.66; // какой operator+()?

return 0;

подмножеств, описанных выше:

}

В   эти   пять   множеств   входят   семь   операторных   функций-кандидатов    на   роль

operator+() в main():

первое множество пусто. В глобальной области видимости, а именно в ней употреблен  operator+() в  функции  main(),  нет  объявлений  перегруженного оператора operator+();

второе множество содержит операторы, объявленные в пространстве имен NS,

где определен класс SmallInt. В этом пространстве имеется один оператор:

NS::SmallInt NS::operator+( const SmallInt &, double );

 

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

SmallInt. Сюда входит

NS::SmallInt NS::operator+( const SmallInt &, int );

четвертое  множество  содержит  операторы,  объявленные  членами  SmallInt.

Такой тоже есть:

NS::SmallInt NS::SmallInt::operator+( const myFloat & );

int operator+( int, int );

double operator+( double, double );

T* operator+( T*, I );

пятое множество содержит встроенные бинарные операторы:

T* operator+( I, T* );

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

15.12.2. Устоявшие функции

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

NS::SmallInt si(15);

использован в следующем контексте:

si + 5.66;

Левый операнд имеет тип SmallInt, а правый – double.

Первый    кандидат    является    устоявшей    функцией    для    данного    использования

operator+():

NS::SmallInt NS::operator+( const SmallInt &, double );

Левый   операнд   типа   SmallInt в   качестве   инициализатора   точно   соответствует формальному параметру-ссылке этого перегруженного оператора. Правый, имеющий тип double, также точно соответствует второму формальному параметру.

Следующая функция-кандидат также устоит:

NS::SmallInt NS::operator+( const SmallInt &, int );

Левый  операнд  si типа  SmallInt в  качестве  инициализатора  точно  соответствует формальному  параметру-ссылке  перегруженного  оператора.  Правый  имеет  тип  int и

 

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

Устоит и третья функция-кандидат:

NS::SmallInt NS::SmallInt::operator+( const myFloat & );

Левый операнд si имеет тип SmallInt, т.е. тип того класса, членом которого является перегруженный оператор. Правый имеет тип int и приводится к типу класса myFloat с помощью определенного пользователем преобразования в виде конструктора myFloat(double).

int operator+( int, int );

Четвертой и пятой устоявшими функциями являются встроенные операторы:

double operator+( double, double );

Класс SmallInt содержит конвертер, который может привести значение типа SmallInt к типу int. Этот конвертер используется вместе с первым встроенным оператором для преобразования  левого  операнда  в  тип  int.  Второй  операнд  типа  double трансформируется  в  тип  int с  помощью  стандартного  преобразования.  Что  касается второго встроенного оператора, то конвертер приводит левый операнд от типа SmallInt к типу int, после чего результат стандартно преобразуется в double. Второй же операнд типа double точно соответствует второму параметру.

Лучшей из этих пяти устоявших функций является первая, operator+(), объявленная в пространстве имен NS:

NS::SmallInt NS::operator+( const SmallInt &, double );

Оба ее операнда точно соответствуют параметрам.

15.12.3. Неоднозначность

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

class String {

// ... public:

String( const char * = 0 );

bool operator== ( const String & ) const;

// нет оператора operator== ( const char * )

сравнения:

};

и такое использование оператора operator==:

 

String flower( "tulip" );

void foo( const char *pf ) {

// вызывается перегруженный оператор String::operator==()

if ( flower == pf )

cout << pf << " is a flower!en";

// ...

}

Тогда при сравнении

flower == pf

вызывается оператор равенства класса String:

String::operator==( const String & ) const;

Для трансформации правого операнда pf из типа const char* в тип String параметра operator==() применяется  определенное  пользователем  преобразование,  которое вызывает конструктор:

String( const char * )

class String {

// ...

public:

String( const char * = 0 );

bool operator== ( const String & ) const;

operator const char*();  // новый конвертер

Если добавить в определение класса String конвертер в тип const char*:

};

// проверка на равенство больше не компилируется!

то показанное использование operator==() становится неоднозначным:

if (flower == pf)

Из-за добавления конвертера operator const char*() встроенный оператор сравнения

bool operator==( const char *, const char * )

тоже считается устоявшей функцией. С его помощью левый операнд flower типа String

может быть преобразован в тип const char *.

Теперь  для  использования  operator==() в  foo() есть  две  устоявших  операторных функции. Первая из них

String::operator==( const String & ) const;

 

требует применения определенного пользователем преобразования правого операнда pf

из типа const char* в тип String. Вторая

bool operator==( const char *, const char * )

требует применения пользовательского преобразования левого операнда flower из типа

String в тип const char*.

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

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

Упражнение 15.17

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

Упражнение 15.18

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

namespace NS {

class complex {

complex( double );

// ...

};

class LongDouble {

friend LongDouble operator+( LongDouble &, int ) { /* ... */ }

public:

LongDouble( int );

operator double();

LongDouble operator+( const complex & );

// ...

};

LongDouble operator+( const LongDouble &, double );

}

int main() {

NS::LongDouble ld(16.08);

double res = ld + 15.05;          // какой operator+?

return 0;

устоявшей функции.

}

 

16