class complex {
double re, im;
public:
complex(double r, double i =0) { re=r; im=i; }

friend complex operator+(complex, complex);
friend complex operator*(complex, complex);

complex operator+=(complex);
complex operator*=(complex);

// ...
};

Все операции над комплексными переменными и целыми константами
с учетом этого описания становятся законными. Целая константа
будет интерпретироваться как комплексное число с мнимой частью,
равной нулю. Так, a=b*2 означает
a = operator*(b, complex( double(2), double(0) ) )
Новые версии операций таких, как + , имеет смысл определять только,
если практика покажет, что повышение эффективности за счет отказа
от преобразований типа стоит того. Например, если выяснится, что
операция умножения комплексной переменной на вещественную
константу является критичной, то к множеству операций можно
добавить operator*=(double):

class complex {
double re, im;
public:
complex(double r, double i =0) { re=r; im=i; }

friend complex operator+(complex, complex);
friend complex operator*(complex, complex);

complex& operator+=(complex);
complex& operator*=(complex);
complex& operator*=(double);

// ...
};

Операции присваивания типа *= и += могут быть очень полезными
для работы с пользовательскими типами, поскольку обычно запись
с ними короче, чем с их обычными "двойниками" * и + , а кроме того
они могут повысить скорость выполнения программы за счет
исключения временных переменных:

inline complex& complex::operator+=(complex a)
{
re += a.re;
im += a.im;
return *this;
}

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

inline complex operator+(complex a, complex b)
{
return complex(a.re+b.re, a.im+b.im);
}

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

matrix& matrix::operator*=(const matrix& a)
{
// ...
return *this;
}

matrix operator*(const matrix& a, const matrix& b)
{
matrix prod = a;
prod *= b;
return prod;
}

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

    7.3.2 Операции преобразования



Конструктор удобно использовать для преобразования типа, но возможны
нежелательные последствия:
[1] Неявные преобразования от пользовательского типа к основному
невозможны (поскольку основные типы не являются классами).
[2] Нельзя задать преобразование из нового типа в старый, не
изменяя описания старого типа.
[3] Нельзя определить конструктор с одним параметром, не определив
тем самым и преобразование типа.
Последнее не является большой проблемой, а первые две можно
преодолеть, если определить операторную функцию преобразования
для исходного типа. Функция-член X::operator T(), где T - имя
типа, определяет преобразование типа X в T. Например, можно
определить тип tiny (крошечный), значения которого находятся в
диапазоне 0..63, и этот тип может в арифметических операциях практически
свободно смешиваться с целыми:

class tiny {
char v;
void assign(int i)
{ if (i>63) { error("выход из диапазона"); v=i&~63; }
v=i;
}
public:
tiny(int i) { assign(i) }
tiny(const tiny& t) { v = t.v; }
tiny& operator=(const tiny& t)
{ v = t.v; return *this; }
tiny& operator=(int i) { assign(i); return *this; }
operator int() { return v; }
};

Попадание в диапазон проверяется как при инициализации объекта
tiny, так и в присваивании ему int. Один объект tiny можно
присвоить другому без контроля диапазона. Для выполнения обычных
операций с целыми для переменных типа tiny определяется функция
tiny::operator int(), производящая неявное преобразование типа
из tiny в int. Там, где требуется int, а задана переменная типа
tiny, используется преобразованное к int значение:

void main()
{
tiny c1 = 2;
tiny c2 = 62;
tiny c3 = c2 -c1; // c3 = 60
tiny c4 = c3; // контроля диапазона нет (он не нужен)
int i = c1 + c2; // i = 64
c1 = c2 + 2 * c1; // выход из диапазона: c1 = 0 (а не 66)
c2 = c1 - i; // выход из диапазона: c2 = 0
c3 = c2; // контроля диапазона нет (он не нужен)
}


Более полезным может оказаться вектор из объектов tiny, поскольку
он позволяет экономить память. Чтобы такой тип было удобно
использовать, можно воспользоваться операцией индексации [].
Пользовательские операции преобразования типа могут пригодиться
для работы с типами, реализующими нестандартные представления чисел
(арифметика с основанием 100, арифметика чисел с фиксированной точкой,
представление в двоично-десятичной записи и т.д.). При этом обычно
приходится переопределять такие операции, как + и *.
Особенно полезными функции преобразования типа оказываются для
работы с такими структурами данных, для которых чтение (реализованное
как операция преобразования) является тривиальным, а присваивание и
инициализация существенно более сложные операции.
Функции преобразования нужны для типов istream и ostream, чтобы
стали возможными, например, такие операторы:

while (cin>>x) cout<<x;

Операция ввода cin>>x возвращает значение istream&. Оно неявно
преобразуется в значение, показывающее состояние потока cin, которое
затем проверяется в операторе while (см. $$10.3.2). Но все-таки
определять неявное преобразование типа, при котором можно потерять
преобразуемое значение, как правило, плохое решение.
Вообще, лучше экономно пользоваться операциями преобразования.
Избыток таких операций может вызывать большое число неоднозначностей.
Транслятор обнаруживает эти неоднозначности, но разрешить их может
быть совсем непросто. Возможно вначале лучше для преобразований
использовать поименованные функции, например, X::intof(), и только
после того, как такую функцию как следуют опробуют, и явное
преобразование типа будет сочтено неэлегантным решением, можно
заменить операторной функцией преобразования X::operator int().

    7.3.3 Неоднозначности



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

class x { /* ... */ x(int); x(char*); };
class y { /* ... */ y(int); };
class z { /* ... */ z(x); };

x f(x);
y f(y);

z g(z);

void k1()
{
f(1); // недопустимо, неоднозначность: f(x(1)) или f(y(1))
f(x(1));
f(y(1));
g("asdf"); // недопустимо, g(z(x("asdf"))) не используется
}

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

class x { /* ... */ x(int); };

void h(double);
void h(x);

void k2()
{
h(1);
}

Вызов h(1) можно интерпретировать либо как h(double(1)), либо как
h(x(1)), поэтому в силу требования однозначности его можно счесть
незаконным. Но поскольку в первой интерпретации используется
только стандартное преобразование, то по правилам, указанным в $$4.6.6
и $$R.13.2, выбирается оно.
Правила на преобразования типа не слишком просто сформулировать
и реализовать, не обладают они и достаточной общностью. Рассмотрим
требование единственности законного преобразования. Проще всего
разрешить транслятору применять любое преобразование, которое он
сумеет найти. Тогда для выяснения корректности выражения не нужно
рассматривать все существующие преобразования. К сожалению, в таком
случае поведение программы будет зависеть от того, какое именно
преобразование найдено. В результате поведение программы будет
зависеть от порядка описаний преобразований. Поскольку часто эти
описания разбросаны по разным исходным файлам (созданным, возможно,
разными программистами), то результат программы будет зависеть
в каком порядке эти файлы сливаются в программу. С другой стороны,
можно вообще запретить неявные преобразования, и это самое
простое решение. Но результатом будет некачественный интерфейс,
определяемый пользователем, или взрывной рост перегруженных
функций и операций, что мы и видели на примере класса complex
из предыдущего раздела.
При самом общем подходе учитываются все сведения
о типах и рассматриваются все существующие преобразования.
Например, с учетом приведенных описаний в присваивании aa=f(1)
можно разобраться с вызовом f(1), поскольку тип aa задает
единственное преобразование. Если aa имеет тип x, то единственным
преобразованием будет f(x(1)), поскольку только оно дает нужный
для левой части тип x. Если aa имеет тип y, будет использоваться
f(y(1)). При самом общем подходе удается разобраться и с вызовом
g("asdf"), поскольку g(z(x("asdf))) является его единственной
интерпретацией. Трудность этого подхода в том, что требуется
доскональный разбор всего выражения, чтобы установить интерпретацию
каждой операции и вызова функции. В результате трансляция
замедляется, вычисление выражения может произойти странным образом
и появляются загадочные сообщения об ошибках, когда транслятор
учитывает определенные в библиотеках преобразования и т.д.
В результате транслятору приходится учитывать больше информации,
чем известно самому программисту! Выбран подход, при котором
проверка является строго восходящим процессом, когда в каждый
момент рассматривается только одна операция с операндами, типы
которых уже прошли проверку.
Требование строго восходящего разбора выражения предполагает,
что тип возвращаемого значения не учитывается при разрешении
перегрузки:

class quad {
// ...
public:
quad(double);
// ...
};

quad operator+(quad,quad);

void f(double a1, double a2)
{
quad r1 = a1+a2; // сложение с двойной точностью
quad r2 = quad(a1)+a2; // вынуждает использовать
// операции с типами quad
}

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

class real {
// ...
public:
operator double();
operator int();
// ...
};

void g(real a)
{
double d = a; // d = a.double();
int i = a; // i = a.int();

d = a; // d = a.double();
i = a; // i = a.int();
}

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

    7.4 Литералы



Для классов нельзя определить литеральные значения, подобному
тому как 1.2 и 12e3 являются литералами типа double. Однако,
для интерпретации значений классов могут использоваться вместо
функций-членов литералы основных типов. Общим средством для
построения таких значений служат конструкторы с единственным
параметром. Если конструктор достаточно простой и реализуется
подстановкой, вполне разумно представлять его вызов как
литерал. Например, с учетом описания класса complex в <complex.h>
в выражении zz1*3+zz2*complex(1,2) произойдет два вызова функций,
а не пять. Две операции * приведут к вызову функции, а операция
+ и вызовы конструктора для построения complex(3) и complex(1,2)
будут реализованы подстановкой.

    7.5 Большие объекты



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

class matrix {
double m[4][4];
public:
matrix();
friend matrix operator+(const matrix&, const matrix&);
friend matrix operator*(const matrix&, const matrix&);
};

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

matrix operator+(const matrix& arg1, const& arg2)
{
matrix sum;
for (int i = 0; i<4; i++)
for (int j=0; j<4; j++)
sum.m[i] [j] = arg1.m[i][j] + arg2.m[i][j];
return sum;
}

Здесь в функции operator+() операнды выбираются по ссылке, а
возвращается само значение объекта. Более эффективным решением
был бы возврат тоже ссылки:

class matrix {
// ...
friend matrix& operator+(const matrix&, const matrix&);
friend matrix& operator*(const matrix&, const matrix&);
};

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

    7.6 Присваивание и инициализация



Рассмотрим простой строковый класс string:

struct string {
char* p;
int size; // размер вектора, на который указывает p

string(int size) { p = new char[size=sz]; }
~string() { delete p; }
};

Строка - это структура данных, содержащая указатель на вектор
символов и размер этого вектора. Вектор создается конструктором и
удаляется деструктором. Но как мы видели в $$5.5.1 здесь могут
возникнуть проблемы:

void f()
{
string s1(10);
string s2(20)
s1 = s2;
}

Здесь будут размещены два символьных вектора, но в результате
присваивания s1 = s2 указатель на один из них будет уничтожен,
и заменится копией второго. По выходе из f() будет вызван для s1
и s2 деструктор, который дважды удалит один и тот же вектор,
результаты чего по всей видимости будут плачевны. Для решения
этой проблемы нужно определить соответствующее присваивание
объектов типа string:

struct string {
char* p;
int size; // размер вектора, на который указывает p

string(int size) { p = new char[size=sz]; }
~string() { delete p; }
string& operator=(const string&);
};

string& string::operator=(const string& a)
{
if (this !=&a) { // опасно, когда s=s
delete p;
p = new char[size=a.size];
strcpy(p,a.p);
}
return *this;
}

При таком определении string предыдущий пример пройдет как
задумано. Но после небольшого изменения в f() проблема возникает
снова, но в ином обличии:

void f()
{
string s1(10);
string s2 = s1; // инициализация, а не присваивание
}

Теперь только один объект типа string строится конструктором
string::string(int), а уничтожаться будет две строки. Дело в
том, что пользовательская операция присваивания не применяется
к неинициализированному объекту. Достаточно взглянуть на функцию
string::operator(), чтобы понять причину этого: указатель p
будет тогда иметь неопределенное, по сути случайное значение.
Как правило, в операции присваивания предполагается, что ее
параметры проинициализированы. Для инициализации типа той, что
приведена в этом примере это не так по определению. Следовательно,
чтобы справиться с инициализацией нужна похожая, но своя функция:

struct string {
char* p;
int size; // размер вектора, на который указывает p

string(int size) { p = new char[size=sz]; }
~string() { delete p; }
string& operator=(const string&);
string(const string&);
};

string::string(const string& a)
{
p=new char[size=sz];
strcpy(p,a.p);
}


Инициализация объекта типа X происходит с помощью конструктора
X(const X&). Мы не перестаем повторять, что присваивание и
инициализация являются разными операциями. Особенно это важно в тех
случаях, когда определен деструктор. Если в классе X есть нетривиальный
деструктор, например, производящий освобождение объекта в свободной
памяти, вероятнее всего, в этом классе потребуется полный набор
функций, чтобы избежать копирования объектов по членам:

class X {
// ...
X(something); // конструктор, создающий объект
X(const X&); // конструктор копирования
operator=(const X&); // присваивание:
// удаление и копирование
~X(); // деструктор, удаляющий объект
};

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

string g(string arg)
{
return arg;
}

main()
{
string s = "asdf";
s = g(s);
}

Очевидно, после вызова g() значение s должно быть "asdf". Не трудно
записать в параметр s копию значения s, для этого надо вызвать
конструктор копирования для string. Для получения еще одной копии
значения s по выходе из g() нужен еще один вызов конструктора
string(const string&). На этот раз инициализируется временная
переменная, которая затем присваивается s. Для оптимизации одну,
но не обе, из подобных операций копирования можно убрать. Естественно,
временные переменные, используемые для таких целей, уничтожаются
надлежащим образом деструктором string::~string() (см. $$R.12.2).
Если в классе X операция присваивания X::operator=(const X&)
и конструктор копирования X::X(const X&) явно не заданы программистом,
недостающие операции будут созданы транслятором. Эти созданные
функции будут копировать по членам для всех членов класса X. Если
члены принимают простые значения, как в случае комплексных чисел,
это, то, что нужно, и созданные функции превратятся в простое и
оптимальное поразрядное копирование. Если для самих членов
определены пользовательские операции копирования, они и будут
вызываться соответствующим образом:

class Record {
string name, address, profession;
// ...
};

void f(Record& r1)
{
Record r2 = r1;
}

Здесь для копирования каждого члена типа string из объекта r1
будет вызываться string::operator=(const string&). В нашем первом
и неполноценном варианте строковый класс имеет член-указатель
и деструктор. Поэтому стандартное копирование по членам для
него почти наверняка неверно. Транслятор может предупреждать
о таких ситуациях.


    7.7 Индексация



Операторная функция operator[] задает для объектов классов
интерпретацию индексации. Второй параметр этой функций (индекс) может
иметь произвольный тип. Это позволяет, например, определять
ассоциативные массивы. В качестве примера можно переписать
определение из $$2.3.10, где ассоциативный массив использовался
в небольшой программе, подсчитывающей число вхождений слов в файле.
Там для этого использовалась функция. Мы определим настоящий тип
ассоциативного массива:

class assoc {
struct pair {
char* name;
int val;
};

pair* vec;
int max;
int free;

assoc(const assoc&); // предотвращает копирование
assoc& operator=(const assoc&); // предотвращает копирование
public:
assoc(int);
int& operator[](const char*);
void print_all();
};

В объекте assoc хранится вектор из структур pair размером max.
В переменной free хранится индекс первого свободного элемента
вектора.
Чтобы предотвратить копирование объектов assoc, конструктор
копирования и операция присваивания описаны как частные. Конструктор
выглядит так:

assoc::assoc(int s)
{
max = (s<16) ? 16 : s;
free = 0;
vec = new pair[max];
}

В реализации используется все тот же неэффективный алгоритм поиска,
что и в $$2.3.10. Но теперь, если вектор переполняется, объект
assoc увеличивается:

#include <string.h>

int& assoc::operator[](const char* p)
/*
работает с множеством пар (структур pair):
проводит поиск p, возвращает ссылку на
целое значение из найденной пары,
создает новую пару, если p не найдено
*/
{
register pair* pp;

for (pp=&vec[free-1]; vec<=pp; pp-- )
if (strcmp(p,pp->name) == 0) return pp->val;
if (free == max) { //переполнение: вектор увеличивается
pair* nvec = new pair[max*2];
for (int i=0; i<max; i++) nvec[i] = vec[i];
delete vec;
vec = nvec;
max = 2*max;
}

pp = &vec[free++];
pp->name = new char[strlen(p)+1];
strcpy(pp->name,p);
pp->val = 0; // начальное значение = 0
return pp->val;
}

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

void assoc::print_all()
{
for (int i = 0; i<free; i++)
cout << vec[i].name << ": " << vec[i].val << '\n';
}

Наконец, можно написать тривиальную программу:

main() // подсчет числа вхождений во входной
// поток каждого слова
{
const MAX = 256; // больше длины самого длинного слова
char buf[MAX];
assoc vec(512);
while (cin>>buf) vec[buf]++;
vec.print_all();
}

Опытные программисты могут заметить, что второй комментарий можно
легко опровергнуть. Решить возникающую здесь проблему предлагается
в упражнении $$7.14 [20]. Дальнейшее развитие понятие ассоциативного
массива получит в $$8.8.
Функция operator[]() должна быть членом класса. Отсюда следует,
что эквивалентность x[y] == y[x] может не выполняться, если
x объект класса. Обычные отношения эквивалентности, справедливые
для операций со встроенными типами, могут не выполняться для
пользовательских типов ($$7.2.2, см. также $$7.9).

    7.8 Вызов функции



Вызов функции, т.е. конструкцию выражение(список-выражений), можно
рассматривать как бинарную операцию, в которой выражение является
левым операндом, а список-выражений - правым. Операцию вызова
можно перегружать как и другие операции. В функции operator()()
список фактических параметров вычисляется и проверяется по типам
согласно обычным правилам передачи параметров. Перегрузка операции
вызова имеет смысл прежде всего для типов, с которыми возможна
только одна операция, а также для тех типов, одна из операций над
которыми имеет настолько важное значение, что все остальные в
большинстве случаев можно не учитывать.
Мы не дали определения итератора для ассоциативного массива
типа assoc. Для этой цели можно определить специальный класс
assoc_iterator, задача которого выдавать элементы из assoc в некотором
порядке. В итераторе необходимо иметь доступ к данным, хранимым
в assoc, поэтому он должен быть описан как friend:

class assoc {
friend class assoc_iterator;
pair* vec;
int max;
int free;
public:
assoc(int);
int& operator[](const char*);
};

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

class assoc_iterator {
const assoc* cs; // массив assoc
int i; // текущий индекс
public:
assoc_iterator(const assoc& s) { cs = &s; i = 0; }
pair* operator()()
{ return (i<cs->free)? &cs->vec[i++] : 0; }
};

Массив assoc объекта assoc_iterator нужно инициализировать, и при каждом
обращении к нему с помощью операторной функции () будет возвращаться
указатель на новую пару (структура pair) из этого массива. При достижении
конца массива возвращается 0:

main() // подсчет числа вхождений во входной
// поток каждого слова
{
const MAX = 256; // больше длины самого длинного слова
char buf[MAX];
assoc vec(512);
while (cin>>buf) vec[buf]++;
assoc_iterator next(vec);
pair* p;
while ( p = next(vec) )
cout << p->name << ": " << p->val << '\n';
}

Итератор подобного вида имеет преимущество перед набором
функций, решающим ту же задачу: итератор может иметь собственные
частные данные, в которых можно хранить информацию о ходе итерации.
Обычно важно и то, что можно одновременно запустить сразу несколько
итераторов одного типа.
Конечно, использование объектов для представления итераторов
непосредственно никак не связано с перегрузкой операций. Одни
предпочитают использовать тип итератора с такими операциями, как
first(), next() и last(), другим больше нравится перегрузка операции
++ , которая позволяет получить итератор, используемый как указатель
(см. $$8.8). Кроме того, операторная функция operator() активно
используется для выделения подстрок и индексации многомерных массивов.
Функция operator() должна быть функцией-членом.

    7.9 Косвенное обращение



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

class Ptr {
// ...
X* operator->();
};

объекты класса Ptr могут использоваться для доступа к членам класса
X также, как для этой цели используются указатели: