Анимация
JavaScript


Главная  Библионтека 

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 [ 67 ] 68 69 70 71 72 73 74 75 76

Аналогия проблеме "сдвиг как вывод" может быть найдена в проектировании компьютерных систем. Большинство проектировщиков аппаратуры были бы счастливы использовать + вместо OR, а * вместо AND, потому что такая запись используется во многих системах проектирования электронных компонентов. Несмотря на это, перегрузка операции operator+() в качестве OR явно не нужна в Си++. К тому же, лексема << означает "сдвиг" в Си и Си++; она не означает "вывод".

Как завершающий пример этой проблемы - я иногда видел реализации класса "множество", определяющие и & со значениями "объединение" и "пересечение". Это может иметь смысл для математика, знакомого с таким стилем записи, но при этом не является выражением ни Си, ни Си++, поэтому будет незнакомо для вашего среднего программиста на Си++ (и вследствие этого с трудом сопровождаться). Амперсанд является сокращением для AND; вы не должны назначать ему произвольное значение. Нет абсолютно ничего плохого в a.Union(b) или a.intersect(b). (Вы не можете использовать a.union(b) со строчной буквой u, потому что union является ключевым словом).

146. Используйте перегрузку операций только для определения операций, имеющих аналог в Си (без сюрпризов)

Перегрузка операций была реализована в языке, прежде всего, для того, чтобы вы могли интегрировать разработанный вами арифметический тип в существующую арифметическую систему языка Си. Этот механизм никогда не предназначался в качестве средства расширения этой системы. Следовательно, перегрузку операций лучше применять, используя классы для реализации лишь арифметических типов.

Тем не менее, также разумно использовать перегруженные операции и там, где аналогии с Си незаметны. Например, большинство классов будет перегружать присваивание. Перегрузка operator==() и operator!=() также разумна в большинстве классов.

Менее ясным (и более противоречивым) примером является класс "итератор". Итератор является средством просмотра каждого члена структуры данных, и он используется почти точно так же, как если бы он был указателем на массив. Например, вы можете в Си итерировать массив, просматривая каждый элемент, следующим образом:

string array[ size ]; string *p = array;

for( int i = size; -- i >=0;)



Операция

Описание

ti = t;

Возврат к началу последовательности

--ti;

Возврат к предыдущему элементу

ti += i;

Переместить вперед на i элементов

ti -= i;

Переместить назад на i элементов

ti + i;

ti-i;

Присваивает итератору другой временной переменной значение с указанным смещением от ti

ti[i];

Элемент со смещением i от текущей позиции

ti[-i];

Элемент со смещением -i от текущей позиции

t2 = ti;

Скопировать позицию из одного итератора в другой

visit( *p++ ); функции visit() передается строка.

Аналог в Си++ может выглядеть вот так (keys является деревом, чьи узлы имеют строковые ключи; здесь могут быть любые другие структуры данных):

tree<string> keys; двоичное дерево с узлами, имеющими

строковые ключи

iterator p = keys; ...

for( int i = keys.sizeO; --i >=0;)

visit( *p++ ); функции visit() передается строка.

Другими словами, вы обращаетесь с деревом как с массивом, и можете итерировать его при помощи итератора, действующего как указатель на элемент. И так как iterator(p) ведет себя точно как указатель в Си, то правило "без сюрпризов" не нарушается.

147. Перегрузив одну операцию, вы должны перегрузить все сходные с ней операции

Это правило является продолжением предыдущего. После того, как вы сказали, что "итератор работает во всем подобно указателю", он на самом деле должен так работать. Пример в предыдущем правиле использовал лишь перегруженные * и ++, но моя настоящая реализация итератора делает аналогию полной, поддерживая все операции с указателями. Таблица 4 показывает различные возможности (t является деревом, а ti - итератором для дерева). Обе операции *++p и *p++ должны работать и т. д. В предыдущем примере я бы должен был также перегрузить в классе tree операции operator [] и (унарная) operator*() для того, чтобы аналогия дерева с массивом выдерживалась везде. Вы уловили эту мысль?

Таблица 4. Перегрузка операторов в итераторе



t2 - ti;

Расстояние между двумя элементами, адресуемыми различными итераторами

ti->msg();

Послать сообщение этому элементу

(*ti).msg();

Послать сообщение этому элементу

Одна из проблем здесь связана с операциями operator==() и operator ! = (), которые при первом взгляде кажутся имеющими смысл в ситуациях, где другие операции сравнения бессмысленны. Например, вы можете использовать == для проверки двух окружностей на равенство, но означает ли равенство "одинаковые координаты и одинаковый радиус", или просто "одинаковый радиус"? Перегрузка других операций сравнения типа < или <= еще более сомнительна, потому что их значение не совсем очевидно. Лучше полностью избегать перегрузки операций, если есть какая-либо неясность в их значении.

148. Перегруженные операции должны работать точно так же, как они работают в Си

Главной новой проблемой здесь являются адресные типы lvalue и rvalue. Выражения типа lvalue легко описываются в терминах Си++: они являются просто ссылками. Компилятор Си, вычисляя выражение, выполняет операции по одной за раз в порядке, определяемом правилами сочетательности и старшинства операций. Каждый этап в вычислениях использует временную переменную, полученную при предыдущей операции. Некоторые операции генерируют "rvalue" - действительные объекты, на самом деле содержащие значение. Другие операции создают "lvalue" - ссылки на объекты. (Кстати, "l" и "r" используются потому, что в выражении l=r слева от = генерируется тип lvalue. Справа образуется тип rvalue).

Вы можете сократить эффект неожиданности для своего читателя, заставив свои перегруженные операции-функции работать тождественно их эквивалентам на Си в пределах того, что они могут. Далее описано, как работают операции Си и как имитировать их поведение:

• Операции присваивания (=, +=, -= и т.д.) и операции автоинкремента и автодекремента (++, --) требуют операндов типа lvalue для адресата - части, которая изменяется. Представьте ++ как эквивалент для + = 1, чтобы понять, почему эта операция в той же категории, что и присваивание.

В перегруженных операциях функций-членов указатель this на самом деле является lvalue, поэтому здесь не о чем беспокоиться. На глобальном уровне левый операнд перегруженной бинарной операции



0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 [ 67 ] 68 69 70 71 72 73 74 75 76