Относительно смысла операций, определяемых пользователем, не
делается никаких предположений. В частности, поскольку не
предполагается, что перегруженное = реализует присваивание ее
первому операнду, не делается никакой проверки, чтобы
удостовериться, является ли этот операнд lvalue (#с.6).
Значения некоторых встроенный операций определены как
равносильные определенным комбинациям другий операций над теми же
аргументами. Например, если a является int, то ++a означает a+=1,
что в свою очередь означает a=a+1. Такие соотношения для
определенных пользователем операций не выполняются, если только не
случилось так, что пользователь сам определил их таким образом.
Например, определение operator+=() для типа complex не может быть
выведено из определений complex::operator+() и
complex::operator=().
По историческому совпадению операции = и & имеют предопределенный
смысл для объектов классов. Никакого элегантного сполоба
"неопределить" эти две операции не существует. Их можно, однако,
сделать недееспособными для класса X. Можно, например, описать
X::operator&(), не задав ее определения. Если где-либо будет
- стр 179 -
браться адрес объекта класса X, то компоновщик обнаружит отсутствие
определения*. Или, другой способ, можно определить X::operator&()
так, чтобы вызывала ошибку во время выполнения.
6.2.3 Операции и Определяемые Пользователем Типы
Функция операция должна или быть членом, или получать в качестве
параметра по меньшей мере один объект класса (функциям, которые
переопределяют операции 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 Определяемое Преобразование Типа
Приведенная во введении реализация комплексных чисел слишком
ограничена, чтобы она могла устроить кого-либо, поэтому ее нужно
расширить. Это будет в основном повторением описанных выше методов.
Например:
____________________
* В некоторых системах компоновщик настолько "умен", что
ругается, даже если неопределена неиспользуемая функция. В таких
системах этим методом воспользоваться нельзя. (прим автора)
- стр 180 -
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);
// ...
};
Теперь, имея описание 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;
}
Но писать функцию для каждого сочетания complex и double, как это
делалось выше для operator+(), невыносимо нудно. Кроме того,
близкие к реальности средства комплексной арифметики должны
предоставлять по меньшей мере дюжину таких функций; посмотрите,
например, на тип complex, описаннчй в .
6.3.1 Конструкторы
Альтенативу использованию нескольких функций (перегруженных)
составлет описание конструктора, который по заданному double
создает complex. Например:
class complex {
// ...
complex(double r) { re=r; im=0; }
};
Конструктор, требующий только один параметр, необязательно вызывать
явно:
complex z1 = complex(23);
complex z2 = 23;
И z1, и z2 будут инициализированы вызовом complex(23).
- стр 181 -
Конструктор - это предписание, как создавать значение данного
типа. Когда требуется значение типа, и когда такое значение может
быть создано конструктором, тогда, если такое значение дается для
присваивания, вызывается конструктор. Например, класс complex можно
было бы описать так:
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 и целые
константы, стали бы допустимы. Целая константа будет
интерпретироваться как complex с нулевой мнимой частью. Например,
a=b*2 означает:
a=operator*( b, complex( double(2), double(0) ) )
Определенное пользователем преобразование типа применяется неявно
только тогда, когда оно является едиственным.
Объект, сконструированный с помощью явного или неявного вызова
конструктора, является автоматическим и будет уничтожен при первой
возможности, обычно сразу же после оператора, в котором он был
создан.
6.3.2 Операции Преобразования
Использование конструктора для задания преобразования типа
является удобным, но имеет следствия, которые могут оказаться
нежелательными:
[1] Не может быть неявного преобразования из определенного
пользователем типа в основной тип (поскольку основные типы не
являются классами);
[2] Невозможно задать преобразование из нового типа в старый, не
изменяя описание старого; и
[3] Невозможно иметь конструктор с одним параметром, не имея при
этом преобразования.
Последнее не является серьезной проблемой, а с первыми двумя
можно справиться, определив для исходного типа операцию
преобразования. Функция член X::operator T(), где T - имя типа,
определяет преобразование из X в T. Например, можно определить тип
tiny (крошечный), который может иметь значение только в диапазоне
0...63, но все равно может свободно сочетаться в целыми в
арифметических операциях:
- стр 182 -
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; }
}
Диапазон значения проверяется всегда, когда tiny инициализируется
int, и всегда, когда ему присваивается int. Одно tiny может
присваиваться другому без проверки диапазона. Чтобы разрешить
выполнять над переменными tiny обычные целые операции, определяется
tiny::operator int(), неявное преобразование из int в tiny. Всегда,
когда в том месте, где требуется 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 выше возвращает istream&. Это значение неявно
преобразуется к значению, которое указывает состояние cin, а уже
это значение может проверяться оператором while (см. #8.4.2).
Однако определять преобразование из оного типа в другой так, что
при этом теряется информация, обычно не стоит.
- стр 183 -
6.3.3 Неоднозначности
Присваивание объекту (или инициализация объекта) класса 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);
Вызов мог бы быть проинтерпретирован или как h(double(1)), или как
h(x(1)), и был бы недупустим по правилу единственности. Но превая
интерпретация использует только стандартное преобразование и она
будет выбрана по правилам, приведеным в #4.6.7.
Правила преобразования не являются ни самыми простыми для
реализации и документации, ни наиболее общими из тех, которые можно
было бы разработать. Возьмем требование единственности
преобразования. Более общий подход разрешил бы компилятору
применять любое преобразование, которое он сможет найти; таким
образом, не нужно было бы рассматривать все возможные
преобразования перед тем, как объявить выражение допустимым. К
сожалению, это означало бы, что смысл программы зависит от того,