}

Назначение до сей поры загадочного параметра типа size_t становится
очевидным. Это - размер освобождаемого объекта. При удалении простого
служащего этот параметр получает значение sizeof(employee), а при
удалении управляющего - sizeof(manager). Поэтому собственные
функции классы для размещения могут не хранить размер каждого
размещаемого объекта. Конечно, они могут хранить эти размеры (подобно
функциям размещения общего назначения) и игнорировать параметр
size_t в вызове operator delete(), но тогда вряд ли они будут лучше,
чем функции размещения и освобождения общего назначения.
Как транслятор определяет нужный размер, который надо передать
функции operator delete()? Пока тип, указанный в operator delete(),
соответствует истинному типу объекта, все просто; но рассмотрим
такой пример:

class manager : public employee {
int level;
// ...
};

void f()
{
employee* p = new manager; // проблема
delete p;
}

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

class employee {
// ...
public:
// ...
void* operator new(size_t);
void operator delete(void*, size_t);
virtual ~employee();
};

Даже пустой деструктор решит нашу проблему:

employee::~employee() { }

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

void f()
{
employee* p = new manager; // теперь без проблем
delete p;
}

Размещение происходит с помощью (созданного транслятором) вызова

employee::operator new(sizeof(manager))

а освобождение с помощью вызова

employee::operator delete(p,sizeof(manager))

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

class X {
// ...
public:
// ...
virtual void f(); // в X есть виртуальная функция, поэтому
// определяем виртуальный деструктор
virtual ~X();
};

    6.7.1 Виртуальные конструкторы



Узнав о виртуальных деструкторах, естественно спросить: "Могут ли
конструкторы то же быть виртуальными?" Если ответить коротко - нет.
Можно дать более длинный ответ: "Нет, но можно легко получить
требуемый эффект".
Конструктор не может быть виртуальным, поскольку для правильного
построения объекта он должен знать его истинный тип. Более того,
конструктор - не совсем обычная функция. Он может взаимодействовать
с функциями управления памятью, что невозможно для обычных
функций. От обычных функций-членов он отличается еще тем, что
не вызывается для существующих объектов. Следовательно нельзя получить
указатель на конструктор.
Но эти ограничения можно обойти, если определить функцию,
содержащую вызов конструктора и возвращающую построенный объект.
Это удачно, поскольку нередко бывает нужно создать новый объект,
не зная его истинного типа. Например, при трансляции иногда
возникает необходимость сделать копию дерева, представляющего
разбираемое выражение. В дереве могут быть узлы выражений разных
видов. Допустим, что узлы, которые содержат повторяющиеся в выражении
операции, нужно копировать только один раз. Тогда нам потребуется
виртуальная функция размножения для узла выражения.
Как правило "виртуальные конструкторы" являются стандартными
конструкторами без параметров или конструкторами копирования,
параметром которых служит тип результата:

class expr {
// ...
public:
expr(); // стандартный конструктор
virtual expr* new_expr() { return new expr(); }
};

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

class conditional : public expr {
// ...
public:
conditional(); // стандартный конструктор
expr* new_expr() { return new conditional(); }
};

Это означает, что, имея объект класса expr, пользователь может
создать объект в "точности такого же типа":

void user(expr* p1, expr* p2)
{
expr* p3 = p1->new_expr();
expr* p4 = p2->new_expr();
// ...
}

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

class expr {
// ...
expr* left;
expr* right;
public:
// ...
// копировать `s' в `this'
inline void copy(expr* s);
// создать копию объекта, на который смотрит this
virtual expr* clone(int deep = 0);
};

Параметр deep показывает различие между копированием собственно
объекта (поверхностное копирование) и копированием всего поддерева,
корнем которого служит объект (глубокое копирование). Стандартное
значение 0 означает поверхностное копирование.
Функцию clone() можно использовать, например, так:

void fct(expr* root)
{
expr* c1 = root->clone(1); // глубокое копирование
expr* c2 = root->clone(); // поверхностное копирование
// ...
}

Являясь виртуальной, функция clone() способна размножать объекты
любого производного от expr класса.
Настоящее копирование можно определить так:

void expr::copy(expression* s, int deep)
{
if (deep == 0) { // копируем только члены
*this = *s;
}
else { // пройдемся по указателям:
left = s->clone(1);
right = s->clone(1);
// ...
}
}

Функция expr::clone() будет вызываться только для объектов типа
expr (но не для производных от expr классов), поэтому можно просто
разместить в ней и возвратить из нее объект типа expr, являющийся
собственной копией:

expr* expr::clone(int deep)
{
expr* r = new expr(); // строим стандартное выражение
r->copy(this,deep); // копируем `*this' в `r'
return r;
}

Такую функцию clone() можно использовать для производных от expr
классов, если в них не появляются члены-данные (а это как раз
типичный случай):

class arithmetic : public expr {
// ...
// новых членов-данных нет =>
// можно использовать уже определенную функцию clone
};

С другой стороны, если добавлены члены-данные, то нужно определять
собственную функцию clone():

class conditional : public expression {
expr* cond;
public:
inline void copy(cond* s, int deep = 0);
expr* clone(int deep = 0);
// ...
};

Функции copy() и clone() определяются подобно своим двойникам из
expression:

expr* conditional::clone(int deep)
{
conditional* r = new conditional();
r->copy(this,deep);
return r;
}

void conditional::copy(expr* s, int deep)
{
if (deep == 0) {
*this = *s;
}
else {
expr::copy(s,1); // копируем часть expr
cond = s->cond->clone(1);
}
}

Определение последней функции показывает отличие настоящего
копирования в expr::copy() от полного размножения в expr::clone()
(т.е. создания нового объекта и копирования в него). Простое
копирование оказывается полезным для определения более сложных
операций копирования и размножения. Различие между copy() и clone()
эквивалентно различию между операцией присваивания и конструктором
копирования ($$1.4.2) и эквивалентно различию между функциями
_draw() и draw() ($$6.5.3). Отметим, что функция copy() не является
виртуальной. Ей и не надо быть таковой, поскольку виртуальна
вызывающая ее функция clone(). Очевидно, что простые операции
копирования можно также определять как функции-подстановки.

    6.7.2 Указание размещения



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

class X {
// ...
public:
X(int);
// ...
};

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

// операция размещения в указанном месте:
void* operator new(size_t, void* p) { return p; }

и задав эти параметры для операции new следующим образом:

char buffer[sizeof(X)];

void f(int i)
{
X* p = new(buffer) X(i); // разместить X в buffer
// ...
}

Функция operator new(), используемая операцией new, выбирается
согласно правилам сопоставления параметров ($$R.13.2). Все
функции operator new() должны иметь первым параметром size_t.
Задаваемый этим параметром размер неявно передается операцией
new.
Определенная нами функция operator new() с задаваемым размещением
является самой простой из функций подобного рода. Можно привести
другой пример функции размещения, выделяющей память из некоторой
заданной области:

class Arena {
// ...
virtual void* alloc(size_t) = 0;
virtual void free(void*) = 0;
};

void operator new(size_t sz, Arena* a)
{
return a.alloc(sz);
}

Теперь можно отводить память для объектов произвольных типов из
различных областей (Arena):

extern Arena* Persistent; // постоянная память
extern Arena* Shared; // разделяемая память

void g(int i)
{
X* p = new(Persistent) X(i); // X в постоянной памяти
X* q = new(Shared) X(i); // X в разделяемой памяти
// ...
}

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

void h(X* p)
{
p->~X(); // вызов деструктора
Persistent->free(p); // освобождение памяти
}

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

    6.8 Упражнения



1. (*1) Пусть есть класс

class base {
public:
virtual void iam() { cout << "base\n"; }
};

Определите два производных от base класса и в каждом определите
функцию iam(), выдающую имя своего класса. Создайте объекты
этих классов и вызовите iam() для них. Присвойте адреса объектов
производных классов указателю типа base* и вызовите iam() с
помощью этих указателей.
2. (*2) Реализуйте примитивы управления экраном ($$6.4.1) разумным
для вашей системы образом.
3. (*2) Определите классы triangle (треугольник) и circle
(окружность).
4. (*2) Определите функцию, рисующую отрезок прямой, соединяющий
две фигуры. Вначале надо найти самые ближайшие точки фигур, а
затем соединить их.
5. (*2) Измените пример с классом shape так, чтобы line было
производным классом от rectangle, или наоборот.
6. (*2) Пусть есть класс

class char_vec {
int sz;
char element [1];
public:
static new_char_vec(int s);
char& operator[] (int i) { return element[i]; }
// ...
};

Определите функцию new_char_vec() для отведения непрерывного
участка памяти для объектов char_vec так, чтобы элементы можно
было индексировать как массив element[]. В каком случае эта
функция вызовет серьезные трудности?
7. (*1) Опишите структуры данных, которые нужны для
примера с классом shape из $$6.4, и объясните, как может
выполняться виртуальный вызов.
8. (*1.5) Опишите структуры данных, которые нужны для примера
с классом satellite из $$6.5, и объясните, как может выполняться
виртуальный вызов.
9. (*2) Опишите структуры данных, которые нужны для примера с
классом window из $$6.5.3, и объясните, как может выполняться
виртуальный вызов.
10. (*2) Опишите класс графических объектов с набором возможных
операций, который будет общим базовым в библиотеке графических
объектов. Исследуйте какие-нибудь графические библиотеки,
чтобы понять, какие операции нужны. Определите класс объектов
базы данных с набором возможных операций, который будет
общим базовым классом объектов, хранящихся как последовательность
полей базы данных. Исследуйте какие-нибудь базы данных, чтобы
понять, какие операции нужны. Определите объект графической
базы данных, используя или не используя множественное
наследование. Обсудите относительные плюсы и минусы обоих
решений.
11. (*2) Напишите вариант функции clone() из $$6.7.1, в котором
размножаемый объект может помещаться в область Arena
($$6.7.2), передаваемую как параметр. Реализуйте простой
класс Arena как производный от Arena.
12. (*2) Пусть есть классы Circle (окружность), Square (квадрат) и
Triangle (треугольник), производные от класса shape. Определите
функцию intersect() с двумя параметрами типа Shape*, которая
вызывает подходящую функцию, чтобы выяснить, пересекаются ли
заданные две фигуры. Для этого в указанных классах нужно
определить соответствующие виртуальные функции. Не тратьте
силы на функцию, которая действительно устанавливает, что
фигуры пересекаются, добейтесь только правильной
последовательности вызовов функций.
13. (*5) Разработайте и реализуйте библиотеку для моделирования,
управляемого событиями. Подсказка: используйте <task.h>.
Там уже устаревшие функции и можно написать лучше. Должен
быть класс task (задача). Объект task должен уметь сохранять
свое состояние и восстанавливать его (для этого можно
определить функции task::save() и task::restore()) и тогда
он может действовать как сопрограмма. Специальные задачи
можно определять как объекты классов, производных от task.
Программу, которую выполняет задача, определите как
виртуальную функцию. Должна быть возможность передавать
параметры новой задаче как параметры ее конструктору или
конструкторам. Должен быть диспетчер, который реализует
понятие виртуального времени. Определите функцию
task::delay(long), которая будет "съедать" виртуальное
время. Важный вопрос разработки: является ли
диспетчер частью класса task, или он должен быть независимым?
Задачи должны иметь возможность общения друг с другом.
Для этой цели разработайте класс queue (очередь). Придумайте
способ, чтобы задача могла ожидать входной поток из нескольких
очередей. Все динамические ошибки должны обрабатываться
единообразно. Как организовать отладку программ, написанных
с помощью такой библиотеки?





    * ГЛАВА 7



Если я выбираю слово, оно значит только то,
что я решу, ни больше и ни меньше.
- Шалтай Болтай


Глава содержит описание механизма перегрузки операций в С++.
Программист может задать интерпретацию операций, когда они
применяются к объектам определенного класса. Помимо арифметических,
логических и операций отношения можно переопределить вызов
функций (), индексацию [], косвенное обращение ->, а также
присваивание и инициализацию. Можно определить явные и скрытые
преобразования между пользовательскими и основными типами. Показано,
как определить класс, объект которого можно копировать и
уничтожать только с помощью специальных, определенных пользователем
функций.

    7.1 Введение



Обычно в программах используются объекты, являющиеся конкретным
представлением абстрактных понятий. Например, в С++ тип данных int
вместе с операциями +, -, *, / и т.д. реализует (хотя и ограниченно)
математическое понятие целого. Обычно с понятием связывается набор
действий, которые реализуются в языке в виде основных операций над
объектами, задаваемых в сжатом, удобном и привычном виде.
К сожалению, в языках программирования непосредственно представляется
только малое число понятий. Так, понятия комплексных чисел, алгебры
матриц, логических сигналов и строк в С++ не имеют непосредственного
выражения. Возможность задать представление сложных объектов вместе
с набором операций, выполняемых над такими объектами,
реализуют в С++ классы. Позволяя программисту определять операции
над объектами классов, мы получаем более удобную и традиционную
систему обозначений для работы с этими объектами по сравнению с той,
в которой все операции задаются как обычные функции. Приведем пример:

class complex {
double re, im;
public:
complex(double r, double i) { re=r; im=i; }
friend complex operator+(complex, complex);
friend complex operator*(complex, complex);
};

Здесь приведена простая реализация понятия комплексного числа, когда
оно представлено парой чисел с плавающей точкой двойной точности,
с которыми можно оперировать только с помощью операций + и *.
Интерпретацию этих операций задает программист в определениях функций
с именами operator+ и operator*. Так, если b и c имеют тип complex,
то b+c означает (по определению) operator+(b,c). Теперь можно
приблизиться к привычной записи комплексных выражений:

void f()
{
complex a = complex(1,3.1);
complex b = complex(1.2,2);
complex c = b;

a = b+c;
b = b+c*a;
c = a*b+complex(1,2);
}

Сохраняются обычные приоритеты операций, поэтому второе выражение
выполняется как b=b+(c*a), а не как b=(b+c)*a.

    7.2 Операторные функции



Можно описать функции, определяющие интерпретацию следующих операций:

+ - * / % ^ & | ~ !
= < > += -= *= /= %= ^= &=
|= << >> >>= <<= == != <= >= &&
|| ++ -- ->* , -> [] () new delete

Последние пять операций означают: косвенное обращение ($$7.9),
индексацию ($$7.7), вызов функции ($$7.8), размещение в свободной
памяти и освобождение ($$3.2.6). Нельзя изменить приоритеты этих
операций, равно как и синтаксические правила для выражений. Так,
нельзя определить унарную операцию % , также как и бинарную операцию
!. Нельзя ввести новые лексемы для обозначения операций, но если
набор операций вас не устраивает, можно воспользоваться привычным
обозначением вызова функции. Поэтому используйте pow(), а не ** .
Эти ограничения можно счесть драконовскими, но более свободные
правила легко приводят к неоднозначности. Допустим, мы определим
операцию ** как возведение в степень, что на первый взгляд кажется
очевидной и простой задачей. Но если как следует подумать, то
возникают вопросы: должны ли операции ** выполняться слева направо
(как в Фортране) или справа налево (как в Алголе)? Как
интерпретировать выражение a**p как a*(*p) или как (a)**(p)?
Именем операторной функции является служебное слово operator, за
которым идет сама операция, например, operator<<. Операторная
функция описывается и вызывается как обычная функция. Использование
символа операции является просто краткой формой записи вызова
операторной функции:

void f(complex a, complex b)
{
complex c = a + b; // краткая форма
complex d = operator+(a,b); // явный вызов
}

С учетом приведенного описания типа complex инициализаторы в этом
примере являются эквивалентными.

    7.2.1 Бинарные и унарные операции



Бинарную операцию можно определить как функцию-член с одним
параметром, или как глобальную функцию с двумя параметрами. Значит,
для любой бинарной операции @ выражение aa @ bb интерпретируется
либо как aa.operator(bb), либо как operator@(aa,bb). Если определены обе
функции, то выбор интерпретации происходит по правилам сопоставления
параметров ($$R.13.2). Префиксная или постфиксная унарная операция
может определяться как функция-член без параметров, или как глобальная
функция с одними параметром. Для любой префиксной унарной операции
@ выражение @aa интерпретируется либо как aa.operator@(), либо как
operator@(aa). Если определены обе функции, то выбор интерпретации
происходит по правилам сопоставления параметров ($$R.13.2). Для
любой постфиксной унарной операции @ выражение @aa интерпретируется
либо как aa.operator@(int), либо как operator@(aa,int). Подробно
это объясняется в $$7.10. Если определены обе функции, то выбор
интерпретации происходит по правилам сопоставления параметров
($$13.2). Операцию можно определить только в соответствии с
синтаксическими правилами, имеющимися для нее в грамматике С++.
В частности, нельзя определить % как унарную операцию, а + как
тернарную. Проиллюстрируем сказанное примерами:

class X {
// члены (неявно используется указатель `this'):

X* operator&(); // префиксная унарная операция &
// (взятие адреса)
X operator&(X); // бинарная операция & (И поразрядное)
X operator++(int); // постфиксный инкремент
X operator&(X,X); // ошибка: & не может быть тернарной
X operator/(); // ошибка: / не может быть унарной
};

// глобальные функции (обычно друзья)

X operator-(X); // префиксный унарный минус
X operator-(X,X); // бинарный минус
X operator--(X&,int); // постфиксный инкремент
X operator-(); // ошибка: нет операнда
X operator-(X,X,X); // ошибка: тернарная операция
X operator%(X); // ошибка: унарная операция %

Операция [] описывается в $$7.7, операция () в $$7.8, операция ->
в $$7.9, а операции ++ и -- в $$7.10.

    7.2.2 Предопределенные свойства операций



Используется только несколько предположений о свойствах пользовательских
операций. В частности, operator=, operator[], operator() и
operator-> должны быть нестатическими функциями-членами. Этим
обеспечивается то, что первый операнд этих операций является адресом.
Для некоторых встроенных операций их интерпретация определяется
как комбинация других операций, выполняемых над теми же операндами.
Так, если a типа int, то ++a означает a+=1, что в свою очередь
означает a=a+1. Такие соотношения не сохраняются для пользовательских
операций, если только пользователь специально не определил их с такой
целью. Так, определение operator+=() для типа complex нельзя вывести
из определений complex::operator+() и complex operator=().
По исторической случайности оказалось, что операции = (присваивание),
&(взятие адреса) и , (операция запятая) обладают предопределенными
свойствами для объектов классов. Но можно закрыть от произвольного
пользователя эти свойства, если описать эти операции как частные:

class X {
// ...
private:
void operator=(const X&);
void operator&();
void operator,(const X&);
// ...
};

void f(X a, X b)
{
a= b; // ошибка: операция = частная
&a; // ошибка: операция & частная
a,b // ошибка: операция , частная
}

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

    7.2.3 Операторные функции и пользовательские типы



Операторная функция должна быть либо членом, либо иметь по крайней
мере один параметр, являющийся объектом класса (для функций,
переопределяющих операции new и delete, это не обязательно). Это
правило гарантирует, что пользователь не сумеет изменить
интерпретацию выражений, не содержащих объектов пользовательского
типа. В частности, нельзя определить операторную функцию, работающую
только с указателями. Этим гарантируется, что в С++ возможны
расширения, но не мутации (не считая операций =, &, и , для объектов
класса).
Операторная функция, имеющая первым параметр основного типа,
не может быть функцией-членом. Так, если мы прибавляем комплексную
переменную aa к целому 2, то при подходящем описании функции-члена
aa+2 можно интерпретировать как aa.operator+(2), но 2+aa так
интерпретировать нельзя, поскольку не существует класса int, для
которого + определяется как 2.operator+(aa). Даже если бы это было
возможно, для интерпретации aa+2 и 2+aa пришлось иметь дело с двумя
разными функциями-членами. Этот пример тривиально записывается
с помощью функций, не являющихся членами.
Каждое выражение проверяется для выявления неоднозначностей.
Если пользовательские операции задают возможную интерпретацию
выражения, оно проверяется в соответствии с правилами $$R.13.2.

    7.3 Пользовательские операции преобразования типа



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

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

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

friend complex operator-(complex, double);
friend complex operator-(complex, double);
friend complex operator-(double, complex);
complex operator-(); // унарный -

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

// ...
};

Имея такое определение комплексного числа, можно писать:

void f()
{
complex a(1,1), b(2,2), c(3,3), d(4,4), e(5,5);
a = -b-c;
b = c*2.0*c;
c = (d+e)*a;
}

Все-таки утомительно, как мы это только что делали для operator*()
писать для каждой комбинации complex и double свою функцию. Более
того, разумные средства для комплексной арифметики должны
предоставлять десятки таких функций (посмотрите, например, как
описан тип complex в <complex.h>).

    7.3.1 Конструкторы



Вместо того, чтобы описывать несколько функций, можно описать
конструктор, который из параметра double создает complex:

class complex {
// ...
complex(double r) { re=r; im=0; }
};

Этим определяется как получить complex, если задан double. Это
традиционный способ расширения вещественной прямой до комплексной
плоскости.
Конструктор с единственным параметром не обязательно вызывать
явно:

complex z1 = complex(23);
complex z2 = 23;

Обе переменные z1 и z2 будут инициализироваться вызовом complex(23).
Конструктор является алгоритмом создания значения заданного типа.
Если требуется значение некоторого типа и существует строящий его
конструктор, параметром которого является это значение, то тогда
этот конструктор и будет использоваться. Так, класс complex можно
было описать следующим образом: