В этой главе описывается аппарат, предоставляемый в C++ для перегрузки операций. Программист может определять смысл операций при их применении к объектам определенного класса. Кроме арифметических, можно определять еще и логические операции, операции сравнения, вызова () и индексирования [], а также можно переопределять присваивание и инициализацию. Можно определить явное и неявное преобразование между определяемыми пользователем и основными типами. Показано, как определить класс, объект которого не может быть никак иначе скопирован или уничтожен кроме как специальными определенными пользователем функциями.
Часто программы работают с объектами, которые являются конкретными представлениями абстрактных понятий. Например, тип данных int в C++ вместе с операциями +, -, *, / и т.д. предоставляет реализацию (ограниченную) математического понятия целых чисел. Такие понятия обычно включают в себя множество операций, которые кратко, удобно и привычно представляют основные действия над объектами. К сожалению, язык программирования может непосредственно поддерживать лишь очень малое число таких понятий. Например, такие понятия, как комплексная арифметика, матричная алгебра, логические сигналы и строки не получили прямой поддержки в C++. Классы дают средство спецификации в C++ представления неэлементарных объектов вместе с множеством действий, которые могут над этими объектами выполняться. Иногда определение того, как действуют операции на объекты классов, позволяет программисту обеспечить более общепринятую и удобную запись для манипуляции объектами классов, чем та, которую можно достичь используя лишь основную функциональную запись. Например:
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); };
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); }
6.2.1 Бинарные и Унарные Операции | |
6.2.2 Предопределенные Значения Операций | |
6.2.3 Операции и Определяемые Пользователем Типы |
Можно описывать функции, определяющие значения следующих операций:
+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= && || ++ -- [] () new delete
void f(complex a, complex b) { complex c = a + b; // сокращенная запись complex d = operator+(a,b); // явный вызов }
Бинарная операция может быть определена или как функция член, получающая один параметр, или как функция друг, получающая два параметра. Таким образом, для любой бинарной операции @ aa@bb может интерпретироваться или как aa.operator@(bb), или как operator@(aa,bb). Если определены обе, то aa@bb является ошибкой. Унарная операция, префиксная или постфиксная, может быть определена или как функция член, не получающая параметров, или как функция друг, получающая один параметр. Таким образом, для любой унарной операции @ aa@ или @aa может интерпретироваться или как aa.operator@(), или как operator@(aa). Если определена и то, и другое, то и aa@ и @aa являются ошибками. Рассмотрим следующие примеры:
class X { // друзья friend X operator-(X); // унарный минус friend X operator-(X,X); // бинарный минус friend X operator-(); // ошибка: нет операндов friend X operator-(X,X,X); // ошибка: тернарная // члены (с неявным первым параметром: this) X* operator&(); // унарное & (взятие адреса) X operator&(X); // бинарное & (операция И) X operator&(X,X); // ошибка: тернарное };
Относительно смысла операций, определяемых пользователем, не
делается никаких предположений. В частности, поскольку не
предполагается, что перегруженное = реализует присваивание ее
первому операнду, не делается никакой проверки, чтобы
удостовериться, является ли этот операнд lvalue (#с.6).
Значения некоторых встроенный операций определены как
равносильные определенным комбинациям других операций над теми же
аргументами. Например, если a является int, то ++a означает a+=1,
что в свою очередь означает a=a+1. Такие соотношения для
определенных пользователем операций не выполняются, если только не
случилось так, что пользователь сам определил их таким образом.
Например, определение operator+=() для типа complex не может быть
выведено из определений complex::operator+() и
complex::operator=().
По историческому совпадению операции = и & имеют предопределенный
смысл для объектов классов. Никакого элегантного способа
"не определить" эти две операции не существует. Их можно, однако,
сделать недееспособными для класса x. Можно, например, описать
x::operator&(), не задав ее определения. Если где-либо будет
браться адрес объекта класса x, то компоновщик обнаружит отсутствие
определения*1. Или, другой способ, можно определить X::operator&() так, чтобы вызывала ошибку во время выполнения.
Функция операция должна или быть членом, или получать в качестве
параметра по меньшей мере один объект класса (функциям, которые
переопределяют операции new и delete, это делать необязательно).
Это правило гарантирует, что пользователь не может изменить смысл
никакого выражения, не включающего в себя определенного
пользователем типа. В частности, невозможно определить функцию,
которая действует исключительно на указатели.
Функция операция, первым параметром которой предполагается
основной тип, не может быть функцией членом. Рассмотрим, например,
сложение комплексной переменной aa с целым 2: aa+2, при подходящим
образом описанной функции члене, может быть проинтерпретировано как
aa.operator+(2), но с 2+aa это не может быть сделано, потому что
нет такого класса int, для которого можно было бы определить + так,
чтобы это означало 2.operator+(aa). Даже если бы такой тип был, то
для того, чтобы обработать и 2+aa и aa+2, понадобилось бы две
различных функции члена. Так как компилятор не знает смысла +,
определенного пользователем, то не может предполагать, что он
коммутативен, и интерпретировать 2+aa как aa+2. С этим примером
могут легко справиться функции друзья.
Все функции операции по определению перегружены. Функция операция
задает новый смысл операции в дополнение к встроенному определению,
и может существовать несколько функций операций с одним и тем же
именем, если в типах их параметров имеются отличия, различимые для
компилятора, чтобы он мог различать их при обращении (см. #4.6.7).
6.3.1 Конструкторы | |
6.3.2 Операции Преобразования | |
6.3.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, complex); 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; }
Альтернативу использованию нескольких функций (перегруженных) составляет описание конструктора, который по заданному double создает complex. Например:
class complex { // ... complex(double r) { re=r; im=0; } };
complex z1 = complex(23); complex z2 = 23;
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); };
a=operator*( b, complex( double(2), double(0) ) )
Использование конструктора для задания преобразования типа
является удобным, но имеет следствия, которые могут оказаться
нежелательными:
[1] Не может быть неявного преобразования из определенного
пользователем типа в основной тип (поскольку основные типы не
являются классами);
[2] Невозможно задать преобразование из нового типа в старый, не
изменяя описание старого; и
[3] Невозможно иметь конструктор с одним параметром, не имея при
этом преобразования.
Последнее не является серьезной проблемой, а с первыми двумя
можно справиться, определив для исходного типа операцию
преобразования. Функция член X::operator T(), где T - имя типа,
определяет преобразование из X в T. Например, можно определить тип
tiny (крошечный), который может иметь значение только в диапазоне
0...63, но все равно может свободно сочетаться в целыми в
арифметических операциях:
class tiny { char v; int assign(int i) { return v = (i&~63) ? (error("ошибка диапазона"),0) : i; } public: tiny(int i) { assign(i); } tiny(tiny& i) { v = t.v; } int operator=(tiny& i) { return v = t.v; } int operator=(int i) { return assign(i); } operator int() { return v; } }
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; // нет проверки диапазона (необязательна) }
Присваивание объекту (или инициализация объекта) класса X
является допустимым, если или присваиваемое значение является X,
или существует единственное преобразование присваиваемого значения
в тип X.
В некоторых случаях значение нужного типа может сконструироваться
с помощью нескольких применений конструкторов или операций
преобразования. Это должно делаться явно; допустим только один
уровень неявных преобразований, определенных пользователем. Иногда
значение нужного типа может быть сконструировано более чем одним
способом. Такие случаи являются недопустимыми. Например:
class x { /* ... */ x(int); x(char*); }; class y { /* ... */ y(int); }; class z { /* ... */ z(x); }; overload f; x f(x); y f(y); z g(z); f(1); // недопустимо: неоднозначность f(x(1)) или f(y(1)) f(x(1)); f(y(1)); g("asdf"); // недопустимо: g(z(x("asdf"))) не пробуется g(z("asdf"));
class x { /* ... */ x(int); } overload h(double), h(x); h(1);
Константы классового типа определить невозможно в том смысле, в каком 1.2 и 12e3 являются константой типа double. Вместо них, однако, часто можно использовать константы основных типов, если их реализация обеспечивается с помощью функций членов. Общий аппарат для этого дают конструкторы, получающие один параметр. Когда конструкторы просты и подставляются inline, имеет смысл рассмотреть в качестве константы вызов конструктора. Если, например, в есть описание класса comlpex, то выражение zz1*3+zz2*comlpex(1,2) даст два вызова функций, а не пять. К двум вызовам функций приведут две операции *, а операция + и конструктор, к которому обращаются для создания comlpex(3) и comlpex(1,2), будут расширены inline.
При каждом применении для comlpex бинарных операций, описанных выше, в функцию, которая реализует операцию, как параметр передается копия каждого операнда. Расходы на копирование каждого double заметны, но с ними вполне можно примириться. К сожалению, не все классы имеют небольшое и удобное представление. Чтобы избежать ненужного копирования, можно описать функции таким образом, чтобы они получали ссылочные параметры. Например:
class matrix { double m[4][4]; public: matrix(); friend matrix operator+(matrix&, matrix&); friend matrix operator*(matrix&, matrix&); };
matrix operator+(matrix&, matrix&); { 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; }
class matrix { // ... friend matrix& operator+(matrix&, matrix&); friend matrix& operator*(matrix&, matrix&); };
Рассмотрим очень простой класс строк string:
struct string { char* p; int size; // размер вектора, на который указывает p string(int sz) { p = new char[size=sz]; } ~string() { delete p; } };
void f() { string s1(10); string s2(20); s1 = s2; }
struct string { char* p; int size; // размер вектора, на который указывает p string(int sz) { p = new char[size=sz]; } ~string() { delete p; } void operator=(string&) }; void string::operator=(string& a) { if (this == &a) return; // остерегаться s=s; delete p; p=new char[size=a.size]; strcpy(p,a.p); }
void f() { string s1(10); s2 = s1; }
struct string { char* p; int size; // размер вектора, на который указывает p string(int sz) { p = new char[size=sz]; } ~string() { delete p; } void operator=(string&) string(string&); }; void string::string(string& a) { p=new char[size=a.size]; strcpy(p,a.p); }
class X { // ... X(something); // конструктор: создает объект X(&X); // конструктор: копирует в инициализации operator=(x&); // присваивание: чистит и копирует ~x(); // деструктор: чистит };
string g(string arg) { return arg; } main() { string s = "asdf"; s = g(s); }
Чтобы задать смысл индексов для объектов класса используется функция operator[]. Второй параметр (индекс) функции operator[] может быть любого типа. Это позволяет определять ассоциативные массивы и т.п. В качестве примера давайте перепишем пример из #2.3.10, где при написании небольшой программы для подсчета числа вхождений слов в файле применялся ассоциативный массив. Там использовалась функция. Здесь определяется надлежащий тип ассоциативного массива:
struct pair { char* name; int val; }; class assoc { pair* vec; int max; int free; public: assoc(int); int& operator[](char*); void print_all(); };
assoc::assoc(int s) { max = (s<16) ? s : 16; free = 0; vec = new pair[max]; }
#include int assoc::operator[](char* p) /* работа с множеством пар "pair": поиск p, возврат ссылки на целую часть его "pair" делает новую "pair", если 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; iname = new char[strlen(p)+1]; strcpy(pp->name,p); pp->val = 0; // начальное значение: 0 return pp->val; }
vouid assoc::print_all() { for (int i = 0; i>buf) vec[buf]++; vec.print_all(); }
Вызов функции, то есть запись выражение(список_выражений), можно
проинтерпретировать как бинарную операцию, и операцию вызова можно
перегружать так же, как и другие операции. Список параметров
функции operator() вычисляется и проверяется в соответствие с
обычными правилами передачи параметров. Перегружающая функция может
оказаться полезной главным образом для определения типов с
единственной операцией и для типов, у которых одна операция
настолько преобладает, что другие в большинстве ситуаций можно не
принимать во внимание.
Для типа ассоциативного массива assoc мы не определили итератор.
Это можно сделать, определив класс assoc_iterator, работа которого
состоит в том, чтобы в определенном порядке поставлять элементы из
assoc. Итератору нужен доступ к данным, которые хранятся в assoc,
поэтому он сделан другом:
class assoc { friend class assoc_iterator; pair* vec; int max; int free; public: assoc(int); int& operator[](char*); };
class assoc_iterator{ assoc* cs; // текущий массив assoc int i; // текущий индекс public: assoc_iterator(assoc& s) { cs = &s; i = 0; } pair* operator()() { return (ifree)? &cs->vec[i++] : 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() ) cout << p->name << ": " << p->val << "\n"; }
Вот довольно реалистичный пример класса string. В нем производится учет ссылок на строку с целью минимизировать копирование и в качестве констант применяются стандартные символьные строки C++.
#include #include class string { struct srep { char* s; // указатель на данные int n; // счетчик ссылок }; srep *p; public: string(char *); // string x = "abc" string(); // string x; string(string &); // string x = string ... string& operator=(char *); string& operator=(string &); ~string(); char& operator[](int i); friend ostream& operator<<(ostream&, string&); friend istream& operator>>(istream&, string&); friend int operator==(string& x, char* s) {return strcmp(x.p->s, s) == 0; } friend int operator==(string& x, string& y) {return strcmp(x.p->s, y.p->s) == 0; } friend int operator!=(string& x, char* s) {return strcmp(x.p->s, s) != 0; } friend int operator!=(string& x, string& y) {return strcmp(x.p->s, y.p->s) != 0; } };
string::string() { p = new srep; p->s = 0; p->n = 1; } string::string(char* s) { p = new srep; p->s = new char[ strlen(s)+1 ]; strcpy(p->s, s); p->n = 1; } string::string(string& x) { x.p->n++; p = x.p; } string::~string() { if (--p->n == 0) { delete p->s; delete p; } }
string& string::operator=(char* s) { if (p->n > 1) { // разъединить себя p-n--; p = new srep; } else if (p->n == 1) delete p->s; p->s = new char[ strlen(s)+1 ]; strcpy(p->s, s); p->n = 1; return *this; }
string& string::operator=(string& x) { x.p->n++; if (--p->n == 0) { delete p->s; delete p; } p = x.p; return *this; }
ostream& operator<<(ostream& s, string& x) { return s << x.p->s << " [" << x.p->n << "]\n"; }
istream& operator>>(istream& s, string& x) { char buf[256]; s >> buf; x = buf; cout << "echo: " << x << "\n"; return s; }
void error(char* p) { cerr << p << "\n"; exit(1); } char& string::operator[](int i) { if (i<0 || strlen(p->s)s[i]; }
main() { string x[100]; int n; cout << "отсюда начнем\n"; for (n = 0; cin>>x[n]; n++) { string y; if (n==100) error("слишком много строк"); cout << (y = x[n]); if (y=="done") break; } cout << "отсюда мы пройдем обратно\n"; for (int i=n-1; 0<=i; i--) cout << x[i]; }
Теперь, наконец, можно обсудить, в каких случаях для доступа к
закрытой части определяемого пользователем типа использовать члены,
а в каких - друзей. Некоторые операции должны быть членами:
конструкторы, деструкторы и виртуальные функции (см. следующую
главу), но обычно это зависит от выбора.
Рассмотрим простой класс X:
class X { // ... X(int); int m(); friend int f(X&); };
void g() { 1.m(); // ошибка f(1); // f(x(1)); }
Как и большая часть возможностей в языках программирования,
перегрузка операций может применяться как правильно, так и
неправильно. В частности, можно так воспользоваться возможность
определять новые значения старых операций, что они станут почти
совсем непостижимы. Представьте, например, с какими сложностями
столкнется человек, читающий программу, в которой операция + была
переопределена для обозначения вычитания.
Данный аппарат должен уберечь программиста/читателя от худших
крайностей применения перегрузки, потому что программист
предохранен от изменения значения операций для основных типов
данных вроде int, а также потому, что синтаксис выражений и
приоритеты операций сохраняются.
Может быть. разумно применять перегрузку операций главным образом
так, чтобы подражать общепринятому применению операций. В тех
случаях, когда нет общепринятой операции или имеющееся в C++
множество операций не подходит для имитации общепринятого
применения, можно использовать запись вызова функции.
struct X { int i; X(int); operator+(int); }; struct Y { int i; Y(X); operator+(X); operator int(); }; X operator* (X,Y); int f(X); X x = 1; Y y = x; int i = 2; main() { i + 10; y + 10; y + 10 * y; x + y + i; x * x + i; f(7); f(y); y + y; 106 + y; }
*1 В некоторых системах компоновщик настолько "умен", что ругается, даже если не определена неиспользуемая функция. В таких системах этим методом воспользоваться нельзя. (прим автора)