В этой главе описывается аппарат, предоставляемый в 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 В некоторых системах компоновщик настолько "умен", что ругается, даже если не определена неиспользуемая функция. В таких системах этим методом воспользоваться нельзя. (прим автора)
Релятивисты и позитивисты утверждают, что "мысленный эксперимент" весьма полезный интрумент для проверки теорий (также возникающих в нашем уме) на непротиворечивость. В этом они обманывают людей, так как любая проверка может осуществляться только независимым от объекта проверки источником. Сам заявитель гипотезы не может быть проверкой своего же заявления, так как причина самого этого заявления есть отсутствие видимых для заявителя противоречий в заявлении.
Это мы видим на примере СТО и ОТО, превратившихся в своеобразный вид религии, управляющей наукой и общественным мнением. Никакое количество фактов, противоречащих им, не может преодолеть формулу Эйнштейна: "Если факт не соответствует теории - измените факт" (В другом варианте " - Факт не соответствует теории? - Тем хуже для факта").
Максимально, на что может претендовать "мысленный эксперимент" - это только на внутреннюю непротиворечивость гипотезы в рамках собственной, часто отнюдь не истинной логики заявителя. Соответсвие практике это не проверяет. Настоящая проверка может состояться только в действительном физическом эксперименте.
Эксперимент на то и эксперимент, что он есть не изощрение мысли, а проверка мысли. Непротиворечивая внутри себя мысль не может сама себя проверить. Это доказано Куртом Гёделем.
Понятие "мысленный эксперимент" придумано специально спекулянтами - релятивистами для шулерской подмены реальной проверки мысли на практике (эксперимента) своим "честным словом". Подробнее читайте в FAQ по эфирной физике.