Конструкторы
Операции Преобразования
Неоднозначности
Приведенная
во введении реализация комплексных чисел слишком ограничена, чтобы она
могла устроить кого-либо, поэтому ее нужно расширить. Это будет в
основном повторением описанных выше методов.
Например:
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.
Конструкторы
Альтернативу
использованию нескольких функций (перегруженных) составляет описание
конструктора, который по заданному double создает complex.
Например:
class complex {
// ...
complex(double r) { re=r; im=0; }
};
Конструктор, требующий только один параметр, необязательно вызывать явно:
complex z1 = complex(23);
complex z2 = 23;
И z1, и z2 будут инициализированы вызовом complex(23).
Конструктор
- это предписание, как создавать значение данного типа. Когда требуется
значение типа, и когда такое значение может быть создано конструктором,
тогда, если такое значение дается для присваивания, вызывается
конструктор.
Например, класс 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) ) )
Определенное пользователем преобразование типа применяется неявно только тогда, когда оно является единственным.
Объект,
сконструированный с помощью явного или неявного вызова конструктора,
является автоматическим и будет уничтожен при первой возможности,
обычно сразу же после оператора, в котором он был создан.
Операции Преобразования
Использование конструктора для задания
преобразования типа является удобным, но имеет следствия, которые могут
оказаться нежелательными:
- Не может быть неявного преобразования из определенного пользователем типа
в основной тип (поскольку основные типы не являются классами);
- Невозможно задать преобразование из нового типа в старый, не изменяя
описание старого; и
- Невозможно иметь конструктор с одним параметром, не имея при этом
преобразования.
Последнее не является серьезной проблемой,
а с первыми двумя можно справиться, определив для исходного типа операцию
преобразования. Функция член 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; }
}
Диапазон значения проверяется всегда, когда
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 . Однако определять
преобразование из оного типа в другой так, что при этом теряется информация,
обычно не стоит.
Неоднозначности
Присваивание объекту (или инициализация
объекта) класса 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)), и был бы недопустим по правилу
единственности. Но первая интерпретация использует только стандартное
преобразование и она будет выбрана по правилам. Правила преобразования не
являются ни самыми простыми для реализации и документации, ни наиболее общими из
тех, которые можно было бы разработать. Возьмем требование единственности
преобразования. Более общий подход разрешил бы компилятору применять любое
преобразование, которое он сможет найти; таким образом, не нужно было бы
рассматривать все возможные преобразования перед тем, как объявить выражение
допустимым. К сожалению, это означало бы, что смысл программы зависит от того,
какое преобразование было найдено. В результате смысл программы неким образом
зависел бы от порядка описания преобразования. Поскольку они часто находятся в
разных исходных файлах (написанных разными людьми), смысл программы будет
зависеть от порядка компоновки этих частей вместе. Есть другой вариант -
запретить все неявные преобразования. Нет ничего проще, но такое правило
приведет либо к неэлегантным пользовательским интерфейсам, либо к бурному росту
перегруженных функций, как это было в предыдущем разделе с complex.
Самый общий подход учитывал бы всю
имеющуюся информацию о типах и рассматривал бы все возможные преобразования.
Например, если использовать предыдущее описание, то можно было бы обработать aa=f(1),
так как тип aa определяет единственность толкования. Если aa является x, то
единственное, дающее в результате x, который требуется присваиванием, - это f(x(1)),
а если aa - это y, то вместо этого будет использоваться f(y(1)). Самый общий
подход справился бы и с g("asdf"), поскольку единственной интерпретацией этого
может быть g(z(x("asdf"))). Сложность этого подхода в том, что он требует
расширенного анализа всего выражения для того, чтобы определить интерпретацию
каждой операции и вызова функции. Это приведет к замедлению компиляции, а также
к вызывающим удивление интерпретациям и сообщениям об ошибках, если компилятор
рассмотрит преобразования, определенные в библиотеках и т.п. При таком подходе
компилятор будет принимать во внимание больше, чем, как можно ожидать, знает
пишущий программу программист!