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: String name; name = "Sherlock"; // использование оператора operator=( char * ) (Операторы присваивания, отличные от копирующих, мы рассмотрим в разделе 15.3.) Во втором наборе есть всего один оператор – взятия индекса: // перегруженный оператор взятия индекса char& operator[]( int ); Он позволяет программе индексировать объекты класса String точно так же, как массивы объектов встроенного типа: if ( name[0] != 'S' ) cout <<"увы, что-то не так\n"; (Детально этот оператор описывается в разделе 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 < < "конкатенация не сработала\n";
}
Перегруженный оператор объявляется в теле класса точно так же, как обычная функция-член, только его имя состоит из ключевого слова 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). Прежде чем закончить этот подраздел, определим операторы равенства для класса 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 ;
}
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. Разработка перегруженных операторовОператоры присваивания, взятия адреса и оператор “запятая” имеют предопределенный смысл, если операндами являются объекты типа класса. Но их можно и перегружать. Семантика всех остальных операторов, когда они применяются к таким операндам, должна быть явно задана разработчиком. Выбор предоставляемых операторов зависит от ожидаемого использования класса. Начинать следует с определения его открытого интерфейса. Набор открытых функций-членов формируется с учетом операций, которые класс должен предоставлять пользователям. Затем принимается решение, какие функции стоит реализовать в виде перегруженных операторов. После определения открытого интерфейса класса проверьте, есть ли логическое соответствие между операциями и операторами:
У каждого оператора есть некоторая естественная семантика. Так, бинарный + всегда ассоциируется со сложением, а его отображение на аналогичную операцию с классом может оказаться удобной и краткой нотацией. Например, для матричного типа сложение двух матриц является вполне подходящим расширением бинарного плюса. Примером неправильного использования перегрузки операторов является определение 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, определенные в области видимости пространства имен. Оператор равенства для двух объектов 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;
}
Сравните это определение с определением того же оператора как функции-члена: 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. В таком случае весь класс Window объявляется другом Screen: class Window;
class Screen {
friend class Window;
// ...
};
К закрытым членам класса Screen теперь можно обращаться из любой функции-члена Window. Упражнение 15.6 Реализуйте операторы ввода и вывода, определенные для класса Screen в упражнении 15.5, в виде друзей и модифицируйте их определения так, чтобы они напрямую обращались к закрытым членам. Какая реализация лучше? Объясните почему. 15.3. Оператор =Присваивание одного объекта другому объекту того же класса выполняется с помощью копирующего оператора присваивания. (Этот специальный случай был рассмотрен в разделе 14.7.) Для класса могут быть определены и другие операторы присваивания. Если объектам класса надо присваивать значения типа, отличного от этого класса, то разрешается определить такие операторы, принимающие подобные параметры. Например, чтобы поддержать присваивание C-строки объекту String: String car ("Volks");
car = "Studebaker";
мы предоставляем оператор, принимающий параметр типа const char*. Эта операция уже была объявлена в нашем классе: class String {
public:
// оператор присваивания для char*
String& operator=( const char * );
// ...
private:
int _size;
char *string;
};
Такой оператор реализуется следующим образом. Если объекту String присваивается нулевой указатель, он становится "пустым". В противном случае ему присваивается копия C-строки: 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;
}
_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. Это может быть массив символов, который модифицируется способом, неизвестным объекту String. Например: char ia[] = { 'd', 'a', 'n', 'c', 'e', 'r' };
String trap = ia; // trap._string ссылается на ia
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 ];
}
В следующем фрагменте нулевому элементу массива color присваивается символ 'V': String color( "violet" ); color[ 0 ] = 'V'; Обратите внимание, что в определении оператора проверяется выход индекса за границы массива. Для этого используется библиотечная C-функция assert(). Можно также возбудить исключение, показывающее, что значение elem меньше 0 или больше длины C-строки, на которую ссылается _string. (Возбуждение и обработка исключений обсуждались в главе 11.) 15.5. Оператор вызова функцииОператор вызова функции может быть перегружен для объектов типа класса. (Мы уже видели, как он используется, при рассмотрении объектов-функций в разделе 12.3.) Если определен класс, представляющий некоторую операцию, то для ее вызова перегружается соответствующий оператор. Например, для взятия абсолютного значения числа типа int можно определить класс absInt: class absInt {
public:
int operator()( int val ) {
int result = val < 0 ? -val : val;
return result;
}
};
Перегруженный оператор operator() должен быть объявлен как функция-член с произвольным числом параметров. Параметры и возвращаемое значение могут иметь любые типы, допустимые для функций (см. разделы 7.2, 7.3 и 7.4). operator() вызывается путем применения списка аргументов к объекту того класса, в котором он определен. Мы рассмотрим, как он используется в одном из обобщенных алгоритмов, описанных в главе 12. В следующем примере обобщенный алгоритм transform() вызывается для применения определенной в absInt операции к каждому элементу вектора ivec, т.е. для замены элемента его абсолютным значением. #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() );
// ...
}
Первый и второй аргументы 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), ведущий себя аналогично встроенным, но поддерживают и некоторую дополнительную функциональность. Допустим, мы хотим определить тип класса для представления указателя на объект Screen (см. главу 13): class ScreenPtr {
// ...
private:
Screen *ptr;
};
Определение ScreenPtr должно быть таким, чтобы объект этого класса гарантировано указывал на объект Screen: в отличие от встроенного указателя, он не может быть нулевым. Тогда приложение сможет пользоваться объектами типа ScreenPtr, не проверяя, указывают ли они на какой-нибудь объект Screen. Для этого нужно определить класс ScreenPtr с конструктором, но без конструктора по умолчанию (детально конструкторы рассматривались в разделе 14.2): class ScreenPtr {
public:
ScreenPtr( const Screen &s ) : ptr( &s ) { }
// ...
};
В любом определении объекта класса ScreenPtr должен присутствовать инициализатор – объект класса Screen, на который будет ссылаться объект ScreenPtr: ScreenPtr p1; // ошибка: у класса ScreenPtr нет конструктора по умолчанию Screen myScreen( 4, 4 ); 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() << " )\n\n";
for ( int ix = 1; ix <= ps->height(); ++ix )
{
for ( int iy = 1; iy <= ps->width(); ++iy )
cout <<ps->get( ix, iy );
cout << "\n";
}
}
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:
ScreenPtr( Screen &s , int arraySize = 0 )
: ptr( &s ), size ( arraySize ), offset( 0 ) { }
private:
int size;
int offset;
Screen *ptr;
};
С помощью этого аргумента задается размер массива. Чтобы сохранить прежнюю функциональность, предусмотрим для него значение по умолчанию, равное нулю. Таким образом, если второй аргумент конструктора опущен, то член size окажется равен 0 и, следовательно, такой объект будет указывать на единственный объект Screen. Объекты нового класса ScreenPtr можно определять следующим образом: Screen myScreen( 4, 4 ); ScreenPtr pobj( myScreen ); // правильно: указывает на один объект const int arrSize = 10; Screen *parray = new Screen[ arrSize ]; 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 <<"не могу инкрементировать указатель для одного объекта\n";
return *ptr;
}
if ( offset >= size - 1 ) {
cerr << "уже в конце массива\n";
return *ptr;
}
++offset;
return *++ptr;
}
Screen& ScreenPtr::operator--()
{
if ( size == 0 ) {
cerr << "не могу декрементировать указатель для одного объекта\n";
return *ptr;
}
if ( offset <= 0 ) {
cerr << "уже в начале массива\n";
return *ptr;
}
--offset;
return *--ptr;
}
Чтобы отличить префиксные операторы от постфиксных, в объявлениях последних имеется дополнительный параметр типа int. В следующем фрагменте объявлены префиксные и постфиксные варианты операторов инкремента и декремента для класса ScreenPtr: class ScreenPtr {
public:
Screen& operator++(); // префиксные операторы
Screen& operator--();
Screen& operator++(int); // постфиксные операторы
Screen& operator--(int);
// ...
};
Ниже приведена возможная реализация постфиксных операторов: Screen& ScreenPtr::operator++(int)
{
if ( size == 0 ) {
cerr << "не могу инкрементировать указатель для одного объекта\n";
return *ptr;
}
if ( offset == size ) {
cerr << "уже на один элемент дальше конца массива\n";
return *ptr;
}
++offset;
return *ptr++;
}
Screen& ScreenPtr::operator--(int)
{
if ( size == 0 ) {
cerr << "не могу декрементировать указатель для одного объекта\n";
return *ptr;
}
if ( offset == -1 ) {
cerr <<"уже на один элемент раньше начала массива\n";
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 В этом фрагменте для вызова mf() из main() есть две устоявшие функции: void mf( double ); void mf( char, char = '\n' );
При выборе наилучшей из устоявших функции преобразования типов, применяемые к каждому фактическому аргументу, ранжируются. Лучшей считается та, для которое все использованные преобразования не хуже, чем для любой другой устоявшей функции, и хотя бы для одного аргумента такое преобразование лучше, чем для всех остальных функций. В предыдущем примере в каждой из двух устоявших функций для приведения типа фактического аргумента к типу формального параметра применено стандартное преобразование. Вызов считается неоднозначным, так как обе функции-члена разрешают его одинаково хорошо. Независимо от вида вызова функции, в множество устоявших могут быть включены как статические, так и нестатические члены: 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:
Рассмотрим эти шаги более детально. Разрешение перегрузки функции не применяется, если все операнды имеют встроенные типы. В таком случае гарантированно употребляется встроенный оператор. (Использование операторов с операндами встроенных типов описано в главе 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+ принимаются во внимание? Потенциально в случае применения операторного синтаксиса с операндами, имеющими тип класса, строится пять множеств кандидатов. Первые три – те же, что и при вызове обычных функций с аргументами типа класса:
SmallInt operator+ ( const SmallInt &, const SmallInt & );
int main() {
SmallInt si(98);
int iobj = 65;
int res = si + iobj; // ::operator+() - функция-кандидат
}
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 функции-друзья, объявленные в области видимости этого класса, включаются в рассмотрение и добавляются к множеству кандидатов. Эти три множества операторных функций-кандидатов формируются точно так же, как и для вызовов обычных функций с аргументами типа класса. Однако при использовании операторного синтаксиса строятся еще два множества:
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(); 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():
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 * ) Если добавить в определение класса String конвертер в тип const char*: class String {
// ...
public:
String( const char * = 0 );
bool operator== ( const String & ) const;
operator 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
Назад Вперед
|