Смекни!
smekni.com

Правила правой руки 17 Замечания для программистов на c 17 Глава 1 (стр. 33 из 43)

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.

Правила преобразования не являются ни самыми простыми для

реализации и документации, ни наиболее общими из тех, которые можно

было бы разработать. Возьмем требование единственности

преобразования. Более общий подход разрешил бы компилятору

применять любое преобразование, которое он сможет найти; таким

образом, не нужно было бы рассматривать все возможные

преобразования перед тем, как объявить выражение допустимым. К

сожалению, это означало бы, что смысл программы зависит от того,

какое преобразование было найдено. В результате смысл программы

неким образом зависел бы от порядка описания преобразования.

Поскольку они часто находятся в разных исходных файлах (написанных

разными людьми), смысл программы будет зависеть от порядка

компоновки этих частей вместе. Есть другой вариант - запретить все

неявные преобразования. Нет ничего проще, но такое правило приведет

либо к неэлегантным пользовательским интефейсам, либо к бурному

- стр 184 -

росту перегруженных функций, как это было в предыдущем разделе с

complex.

Самый общий подход учитывал бы всю имеющуюся информацию о типах и

рассматривал бы все возможные преобразования. Например, если

использовать предыдущее описание, то можно было бы обработать

aa=f(1), так как тип aa определяет едиственность толкования. Если

aa является x, то единственное, дающее в результате x, который

требеутся присваиванием, - это f(x(1)), а если aa - это y, то

вместо этого будет использоваться f(y(1)). Самый общий подход

справился бы и с g("asdf"), поскольку единственной интерпретацией

этого может быть g(z(x("asdf"))). Сложность этого подхода в том,

что он требует расширенного анализа всего выражения для того, чтобы

определить интерпретацию каждой операции и вызова функции. Это

приведет к замеделению компиляции, а также к вызывающим удивление

интерпретациям и сообщениям об ошибках, если компилятор рассмотрит

преобразования, определенные в библиотеках и т.п. При таком подходе

компилятор будет принимать во внимание больше, чем, как можно

ожидать, знает пишущий программу программист!

6.4 Константы

Константы классового типа определить невозможно в том смысле, в

каком 1.2 и 12e3 являются константой типа double. Вместо них,

однако, часто можно использовать константы основных типов, если их

реализация обеспечивается с помощью функций членов. Общий аппарат

для этого дают конструкторы, получающие один параметр. Когда

конструкторы просты и подставляются inline, имеет смысл рассмотреть

в качестве константы вызов конструктора. Если, например, в

есть описание класса comlpex, то выражение

zz1*3+zz2*comlpex(1,2) даст два вызова функций, а не пять. К двум

вызовам функций приведут две операции *, а операция + и

конструктор, к которому обращаются для создания comlpex(3) и

comlpex(1,2), будут расширены inline.

6.5 Большие Объеты

При каждом применении для comlpex бинарных операций, описанных

выше, в функцию, которая реализует операцию, как параметр

передается копия каждого операнда. Расходы на копирование каждого

double заметны, но с ними вполне можно примириться. К сожалению, не

все классы имеют небольшое и удобное представление. Чтобы избежать

ненужного копирования, можно описать функции таким образом, чтобы

они получали ссылочные параметры. Например:

class matrix {

double m[4][4];

public:

matrix();

friend matrix operator+(matrix&, matrix&);

friend matrix operator*(matrix&, matrix&);

};

Ссылки позволяют использовать выражения, содержашие обычные

арифметические операции над большими объектами, без ненужного

- стр 185 -

копирования. Указатели применять нельзя, потому что невозможно для

применения к указателю смысл операции переопределить невозможно.

Операцию плюс можно определить так:

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;

}

Эта operator+() обращается к операндым + через ссылки, но

возвращает значение объекта. Возврат сылки может оказаться более

эффективным:

class matrix {

// ...

friend matrix& operator+(matrix&, matrix&);

friend matrix& operator*(matrix&, matrix&);

};

Это является допустимым, но приводит к сложности с выделением

памяти. Поскольку ссылка на результат будет передаваться из функции

как ссылка на возвращаетмое значение, оно не может быть

автоматической переменной. Поскольку часто операция используется в

выражении больше одного раза, результат не может быть и статической

переменной. Как правило, его размещают в свободной памяти. Часто

копирование возвращаемого значения окаывается дешевле (по времени

выполнения, объему кода и объему данных) и проще программируется.

6.6 Присваивание и Инициализация

Рассмотрим очень простой класс строк string:

struct string {

char* p;

int size; // размер вектора, на который указывает p

string(int sz) { p = new char[size=sz]; }

~string() { delete p; }

};

Строка - это структура данных, состоящая из вектора символов и

длины этого вектора. Вектор создается конструктором и уничтожается

деструктором. Однако, как показано в #5.10, это может привести к

неприятностям. Например:

void f()

{

string s1(10);

string s2(20);

s1 = s2;

}

- стр 186 -

будет размещать два вектора символов, а присваивание s1=s2 будет

портить указатель на один из них и дублировать другой. На выходе из

f() для s1 и s2 будет вызываться деструктор и уничтожать один и тот

же вектор с непредсказуемо разрушительными последствиями. Решение

этой проблемы состоит в том, чтобы соответствующим образом

определить присваивание объектов типа string:

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);

}

Это определение string гарантирует,и что предыдущий пример будет

работать как предполагалось. Однако небольшое изменение f()

приведет к появлению той же проблемы в новом облике:

void f()

{

string s1(10);

s2 = s1;

}

Теперь создается только одна строка, а уничтожается две. К

ненинициализированному объекту определенная пользователем операция

присваивания не применяется. Беглый взгляд на string::operator=()

объясняет, почему было неразумно так делать: указатель p будет

содержать неопределенное и совершенно случайное значение. Часто

операция присваивания полагается на то, что ее аргументы

инициализириованы. Для такой инициализации, как здесь, это не так

по определению. Следовательно, нужно определить похожую, но другую,

функцию, чтобы обрабатывать инициализацию:

- стр 187 -

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];