CHAP8_3 (1018811)
Текст из файла
Глава 8.Е(F). Перегрузка операций
145. Операция - это сокращение (без сюрпризов).
Операция - это не произвольный значок, означающий все, что вы ни пожелаете. Это аббревиатура англоязычного слова. Например, символ + значит "прибавить", поэтому вы не должны заставлять перегруженный operator+() делать что-нибудь еще. Хотя здесь все ясно (вы можете определить a + b для вычитания b из a, но не должны делать этого), я на самом деле веду речь о более проблемах более творческого характера.
Вы можете благоразумно доказывать, что, когда выполняете конкатенацию, то "прибавляете" одну строку к концу другой, поэтому перегрузка + для конкатенации может быть приемлема. Вы также можете доказывать, что разумно использовать операции сравнения для лексикографического упорядочивания в классе string, поэтому перегрузка операций <, == и т.д. также вероятно пойдет. Вы не сможете аргументированно доказать, что - или * имеют какой-нибудь смысл по отношению к строкам.
Другим хорошим примером того, как нельзя действовать, является интерфейс С++ iostream. Использование сдвига (<<) для обозначения "вывод" является нелепым. Ваши функции вывода в С назывались printf(), а не shiftf(). Я понимаю, что Страуструп выбрал сдвиг, потому что он сходен с механизмом перенаправления ввода/вывода различных оболочек UNIX, но этот довод на самом деле не выдерживает проверки. Страуструп исходил из того, что все программисты на С++ понимают перенаправление в стиле UNIX, но эта концепция отсутствует в некоторых операционных системах - например, в Microsoft Windows. К тому же, для того, чтобы аналогия была полной, операция > должна быть перегружена для выполнения операции затирания, а >> - добавления в конец. Тем не менее, тот факт, что > и >> имеют различный приоритет, делает реализацию такого поведения затруднительной. Дело осложняется тем, что операторы сдвига имеют неправильный уровень приоритета. Оператор типа cout << x += 1 не будет работать так, как вы ожидаете, потому что у << более высокий приоритет, чем у +=, поэтому оператор интерпретируется как (cout << x) += 1, что неверно. С++ нуждается в расширяемости, обеспечиваемой системой iostream, но он вынужден добиваться ее за счет введения операторов "ввода" и "вывода", имеющих низший приоритет по отношению к любому оператору языка.
Аналогия проблеме "сдвиг как вывод" может быть найдена в проектировании компьютерных систем. Большинство проектировщиков аппаратуры были бы счастливы использовать + вместо 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 ; )
visit( *p++ ); // функции visit() передается строка.
Аналог в С++ может выглядеть вот так (keys является деревом, чьи узлы имеют строковые ключи; здесь могут быть любые другие структуры данных):
tree<string> keys; // двоичное дерево с узлами, имеющими строковые ключи
iterator p = keys;
// ...
for( int i = keys.size(); --i >= 0 ; )
visit( *p++ ); // функции visit() передается строка.
Другими словами, вы обращаетесь с деревом как с массивом, и можете итерировать его при помощи итератора, действующего как указатель на элемент. И так как iterator(p) ведет себя точно как указатель в С, то правило "без сюрпризов" не нарушается.
147. Перегрузив одну операцию, вы должны перегрузить все сходные с ней операции.
Это правило является продолжением предыдущего. После того, как вы сказали, что "итератор работает совсем подобно указателю", он на самом деле должен так работать. Пример в предыдущем правиле использовал лишь перегруженные * и ++, но моя настоящая реализация итератора делает аналогию полной, поддерживая все операции с указателями. Таблица 4 показывает различные возможности (t является деревом, а ti - итератором для дерева). Обе операции *++p и *p++ должны работать и т.д. В предыдущем примере я бы должен был также перегрузить в классе tree операции operator[] и (унарная)operator*() для того, чтобы аналогия дерева с массивом выдерживалась везде. Вы уловили эту мысль.
Таблица 4. Перегрузка операторов в итераторе.
| Операция | Описание |
| ti = t; | Возврат к началу последовательности |
| --ti; | Возврат к предыдущему элементу |
| ti += i; | Переместить вперед на i элементов |
| ti -= i; | Переместить назад на i элементов |
| ti + i; ti - i; | Присваивает итератору другой временной переменной значение с указанным смещением от ti |
| ti[i]; | Элемент со смещением i от текущей позиции |
| ti[-i]; | Элемент со смещением -i от текущей позиции |
| t2 = ti; | Скопировать позицию из одного итератора в другой |
| t2 - ti; | Расстояние между двумя элементами, адресуемыми различными итераторами |
| ti->msg(); | Послать сообщение этому элементу |
| (*ti).msg(); | Послать сообщение этому элементу |
Одна из проблем здесь связана с операциями operator==() и operator!=(), которые при первом взгляде кажутся имеющими смысл в ситуациях, где другие операции сравнения бессмысленны. Например, вы можете использовать == для проверки двух окружностей на равенство, но означает ли равенство "одинаковые координаты и одинаковый радиус", или просто "одинаковый радиус"? Перегрузка других операций сравнения типа < или <= еще более сомнительна, потому что их значение не совсем очевидно. Лучше полностью избегать перегрузки операций, если есть какая-либо неясность в их значении.
148. Перегруженные операции должны работать точно так же, как они работают в С.
Главной новой проблемой здесь являются адресные типы lvalue и rvalue. Выражения типа lvalue легко описываются в терминах С++: они являются просто ссылками. Компилятор С, вычисляя выражение, выполняет операции по одной за раз в порядке, определяемом правилами сочетательности и старшинства операций. Каждый этап в вычислениях использует временную переменную, полученную при предыдущей операции. Некоторые операции генерируют "rvalue" - действительные объекты, на самом деле содержащие значение. Другие операции создают "lvalue" - ссылки на объекты. (Кстати, "l" и "r" используются потому, что в выражении l=r слева от = генерируется тип lvalue. Справа образуется тип rvalue).
Вы можете сократить эффект неожиданности для своего читателя, заставив свои перегруженные операции-функции работать тождественно их эквивалентам на С в пределах того, что они могут. Далее описано, как работают операции С и как имитировать их поведение:
-
Операции присваивания (=, +=, -= и т.д.) и операции автоинкремента и автодекремента (++, --) требуют операндов типа lvalue для адресата - части, которая изменяется. Представьте ++ как эквивалент для +=1, чтобы понять, почему эта операция в той же категории, что и присваивание.
В перегруженных операциях функций-членов указатель this на самом деле является lvalue, поэтому здесь не о чем беспокоиться. На глобальном уровне левый операнд перегруженной бинарной операции присваивания (и единственный операнд перегруженной унарной операции присваивания) должен быть ссылкой.
-
Все другие операции могут иметь операнды как типа lvalue, так и rvalue.
Используйте ссылку на объект типа const для всех операндов. (Вы могли бы передавать операторы по значению, но обычно это менее эффективно).
-
Имена переменных составного типа (массивов) создают типы rvalue - временные переменные типа указателя на первый элемент, после инициализации на него и указывающие. Заметьте, что неверно представление о том, что вы не можете инкрементировать имя массива из-за того, что оно является константой. Вы не можете инкрементировать имя массива, потому что оно имеет тип rvalue, а все операции инкремента требуют операндов типа lvalue.
-
Имена переменных несоставного типа дают lvalue.
-
Операции *, -> и [] генерируют lvalue, когда относятся к несоставной переменной, иначе они работают подобно именам составных переменных. Если y не является массивом, то x->y создает тип lvalue, который ссылается на этого поле данных. Если y - массив, то x->y генерирует тип rvalue, который ссылается на первую ячейку этого массива.
В С++ перегруженные * и [] должны возвращать ссылки на указанный объект. Операция operator-> таинственна. Правила по существу заставляют вас использовать ее таким же образом, как вы делали бы это в С. Операция -> рассматривается как унарная с операндом слева от нее. Перегруженная функция должна возвращать указатель на что-нибудь, имеющее поля -, структуру класс или объединение. Компилятор будет затем использовать такое поле для получения lvalue или rvalue. Вы не можете перегрузить .(точку).
-
Все другие операнды генерируют тип rvalue.
Эквивалентные перегруженные операции должны возвращать объекты, а не ссылки или указатели.
149. Перегруженной бинарной операции лучше всего быть встроенным (inline) псевдонимом операции приведения типа.
Это правило относится к числу тех, которые будут изменены с улучшением качества компиляторов. Рассмотрим следующее, простое для понимания дополнение к классу string из листинга 7 на странице 111.
class string
{
enum special_ { special };
string( special_ ) {}; // ничего не делает.
// ...
public:
const string operator+( const string &r ) const;
// ...
};
//--------------------------------------------------------------
const string::operator+( const string &r ) const
{
string tmp( special ); // создать пустой объект
tmp.buf = new char[ strlen(buf) + strlen(r.buf) + 1 ];
strcpy( tmp.buf, buf );
strcat( tmp.buf, r.buf );
return tmp;
}
Многие компиляторы, получив вышеуказанное, генерируют довольно неэффективный код. Объект tmp должен инициализироваться при вызове конструктора; здесь это не очень дорого, но обычно это ведет к значительно большим расходам. Конструктор копии должен быть вызван для выполнения оператора return, и сам объект также должен быть уничтожен.
Иногда вы можете улучшить такое поведение путем перегрузки встроенного псевдонима для операции приведения типа:
class string
{
string(const char *left, const char *right );
public:
const string string::operator+( const string &r ) const ;
};
//--------------------------------------------------------------
Характеристики
Тип файла документ
Документы такого типа открываются такими программами, как Microsoft Office Word на компьютерах Windows, Apple Pages на компьютерах Mac, Open Office - бесплатная альтернатива на различных платформах, в том числе Linux. Наиболее простым и современным решением будут Google документы, так как открываются онлайн без скачивания прямо в браузере на любой платформе. Существуют российские качественные аналоги, например от Яндекса.
Будьте внимательны на мобильных устройствах, так как там используются упрощённый функционал даже в официальном приложении от Microsoft, поэтому для просмотра скачивайте PDF-версию. А если нужно редактировать файл, то используйте оригинальный файл.
Файлы такого типа обычно разбиты на страницы, а текст может быть форматированным (жирный, курсив, выбор шрифта, таблицы и т.п.), а также в него можно добавлять изображения. Формат идеально подходит для рефератов, докладов и РПЗ курсовых проектов, которые необходимо распечатать. Кстати перед печатью также сохраняйте файл в PDF, так как принтер может начудить со шрифтами.















