|
||||
|
&&, ||и ,(запятой) Функции и операторы
Функции, включая перегруженные операторы, представляют собой фундаментальные единицы работы. Как вы увидите позже в разделе "Обработка ошибок и исключения" (в частности, в рекомендации 70), это непосредственно влияет на наши рассуждения о корректности и безопасности кода. Но давайте сначала рассмотрим некоторые фундаментальные вопросы написания функций, в том числе операторов. В частности, мы обратим особое внимание на их параметры, семантику и перегрузку. В этом разделе наиболее важной нам представляется рекомендация 26 — "Сохраняйте естественную семантику перегруженных операторов". 25. Передача параметров по значению, (интеллектуальному) указателю или ссылкеРезюмеВы должны четко уяснить разницу между входными, выходными параметрами и параметрами, предназначенными и для ввода, и для вывода информации, а также между передачей параметров по значению и по ссылке, и корректно их использовать. ОбсуждениеПравильный выбор способа передачи аргументов в функцию — по значению, ссылке или с использованием указателей — весьма необходимый навык, который поможет вам сделать ваш код максимально безопасным и эффективным. Хотя эффективность не должна быть нашей главной целью (см. рекомендацию 8), при прочих равных условиях, включая понятность кода, мы не должны без крайней необходимости снижать его эффективность (см. рекомендацию 9). Вам стоит следовать приведенным ниже рекомендациям при выборе способа передачи параметров. Вот рекомендации для входных параметров (которые передают информацию в функцию, но не возвращают ее). • Всегда описывайте все указатели или ссылки на входные параметры как const. • Предпочитайте передачу примитивных типов (например, charили float) и объектов-значений с недорогими операциями копирования (например, Pointили Complex<float>) по значению. • Входные аргументы прочих пользовательских типов лучше передавать как ссылки на const. • Подумайте о передаче по значению вместо передачи по ссылке, если функция требует создания копии аргумента. Концептуально передача по значению эквивалентна передаче ссылки на константный объект и выполнение копирования, и может помочь компилятору в выполнении оптимизации по устранению временных переменных. Далее приведены рекомендации для выходных параметров (а также параметров для одновременного ввода и вывода информации). • Если аргумент необязателен, лучше передать его как (интеллектуальный) указатель (что позволит вызывающей функции передать нулевое значение как указание, что аргумент отсутствует); тот же совет применим и в случае, если функция сохраняет копию указателя или как-то иначе работает с принадлежностью аргумента. • Если аргумент обязательный и функция не сохраняет указатель на него или каким-то иным образом влияет на его принадлежность, то такой аргумент лучше передавать по ссылке. Это указывает, что наличие данного аргумента обязательно, и заставляет вызывающую функцию отвечать за предоставление корректного объекта. Не используйте функции с переменным количеством аргументов (см. рекомендацию 98). Ссылки[Alexandrescu03a] • [Cline99] §2.10-11, 14.02-12, 32.08 • [Dewhurst03] §57 • [Koenig97] §4 • [Lakos96] §9.1.11-12 • [McConnell93] §5.7 • [Meyers97] §21-22 • [Stroustrup94] §11.4.4 • [Stroustrup00] §5.5, §11.6, §16.3.4 • [Sutter00] §6, §46 26. Сохраняйте естественную семантику перегруженных операторовРезюмеПрограммисты ненавидят сюрпризы. Перегружайте операторы только в случае веских на то оснований, и сохраняйте при этом их естественную семантику. Если это оказывается сложным, возможно, вы неверно используете перегрузку операторов. ОбсуждениеХотя все (как мы надеемся) согласны с тем, что не следует реализовывать вычитание как оператор operator+, прочие ситуации не столь очевидны. Например, означает ли оператор operator*вашего класса Tensorскалярное или векторное умножение? Должен ли оператор operator+=(Tensor& t, unsigned u)прибавлять uк каждому из элементов t, или должен изменять размер t? В таких неоднозначных или не поддающихся интуитивному пониманию случаях следует использовать именованные функции, а не прибегать к шифрованию. Для типов-значений (но не для всех типов; см. рекомендацию 32) следует придерживаться правила: "Если не знаешь, как поступить — поступай так, как int" [Meyers96]. Подражание поведению операторов встроенных типов и взаимоотношениям между ними гарантирует, что вы никого не приведете в замешательство. Если выбранная вами семантика заставляет кого-то удивленно поднять брови, может быть, перегрузка оператора — не самая лучшая идея? Программисты ожидают, что операторы идут в связке — если выражение a@bимеет определенный смысл для некоторого определенного вами оператора @(возможно, после преобразования типов), то задайте сами себе вопрос: можно ли написать b@aбез неприятных последствий? Можно ли написать a@=b? (См. рекомендацию 27.) Если оператор имеет обратный оператор (например, +и -, *и /), то поддерживаются ли они оба? От именованных функций не ожидается наличие соответствующих взаимоотношений, так что для большей ясности кода, если возможно неверное истолкование семантики перегруженных операторов, лучше использовать именно функции. ИсключенияИмеются высокоспециализированные библиотеки (например, генераторы синтаксических анализаторов), в предметной области которых соглашения о семантике операторов существенно отличаются от их значений в С++ (например, при работе с регулярными выражениями оператор operator*может использоваться для выражения "ноль или большее количество"). Предпочтительно найти альтернативу необычной перегрузке оператора (например, в регулярных выражениях [C++TR104] используются строки, так что *может использоваться естественным образом, без перегрузки операторов). Если все же после тщательных размышлений вы решили использовать операторы, убедитесь, что вы четко определили единую схему для всех ваших соглашений, и при этом не затеяли опасные игры со встроенными операторами. Ссылки [Cline99] §23.02-06 • [C++TR104] §7 • [Dewhurst03] §85-86 • [Koenig97] §4 • [Lakos96] §9.1.1 • [Meyers96] §6 • [Stroustrup00] §11.1 • [Sutter00] §41 27. Отдавайте предпочтение каноническим формам арифметических операторов и операторов присваиванияРезюмеЕсли можно записать а+b, то необходимо, чтобы можно было записать и a+=b. При определении бинарных арифметических операторов одновременно предоставляйте и их присваивающие версии, причем делайте это с минимальным дублированием и максимальной эффективностью. Обсуждение В общем случае для некоторого бинарного оператора @( +, -, *и т.д.) вы должны также определить его присваивающую версию, так чтобы a@=bи a=a@bимели один и тот же смысл (причем первая версия может быть более эффективна). Канонический способ достижения данной цели состоит в определении @посредством @=следующим образом: T& T::operator@=(const T&) { // ... реализация ... return *this; } T operator@(const T& lhs, const T& rhs) { T temp(lhs); return temp @= rhs; } Эти две функции работают в тандеме. Версия оператора с присваиванием выполняет всю необходимую работу и возвращает свой левый параметр. Версия без присваивания создает временную переменную из левого аргумента, и модифицирует ее с использованием формы оператора с присваиванием, после чего возвращает эту временную переменную. Обратите внимание, что здесь operator@— функция-не член, так что она обладает желательным свойством возможности неявного преобразования как левого, так и правого параметра (см. рекомендацию 44). Например, если вы определите класс String, который имеет неявный конструктор, получающий аргумент типа char, то оператор operator+(const String&, const String&), который не является членом класса, позволяет осуществлять операции как типа char+String, так и String+char; функция-член String::operator+(const String&)позволяет использовать только операцию String+char. Реализация, основной целью которой является эффективность, может определить ряд перегрузок оператора operator@, не являющихся членами класса, чтобы избежать увеличения количества временных переменных в процессе преобразований типов (см. рекомендацию 29). Также делайте не членом функцию operator@=везде, где это возможно (см. рекомендацию 44). В любом случае, все операторы, не являющиеся членами, должны быть помещены в то же пространство имен, что и класс T, так что они будут легко доступны для вызывающих функций при отсутствии каких-либо сюрпризов со стороны поиска имен (см. рекомендацию 57). Как вариант можно предусмотреть оператор operator@, принимающий первый параметр по значению. Таким образом вы обеспечите неявное копирование компилятором, что обеспечит ему большую свободу действий по оптимизации: T& operator@=(T& lhs, const T& rhs) { // ... реализация ... return lhs; } T operator@(T lhs, const T& rhs) { // lhs передано по значению return lhs @= rhs; } Еще один вариант — оператор operator@, который возвращает const-значение. Эта методика имеет то преимущество, что при этом запрещается такой не имеющий смысла код, как a+b=c, но в этом случае мы теряем возможность применения потенциально полезных конструкций наподобие а = (b+c).replace(pos, n, d). А это весьма выразительный код, который в одной строчке выполняет конкатенацию строк bи с, заменяет некоторые символы и присваивает полученный результат переменной а. Примеры Пример. Реализация +=для строк. При конкатенации строк полезно заранее знать длину, чтобы выделять память только один раз: String& String::operator+=( const String& rhs ) { // ... Реализация ... return *this; } String operator+( const String& lhs, const String& rhs ) { String temp; // изначально пуста // выделение достаточного количества памяти temp.Reserve(lhs.size() + rhs.size()); // Конкатенация строк и возврат return (temp += lhs) += rhs; }Исключения В некоторых случаях (например, оператор operator*=для комплексных чисел), оператор может изменять левый аргумент настолько существенно, что более выгодным может оказаться реализация оператора operator*=посредством оператора operator*, а не наоборот. Ссылки [Alexandrescu03a] • [Cline99] §23.06 • [Meyers96] §22 • [Sutter00] §20 28. Предпочитайте канонический вид ++ и --, и вызов префиксных операторовРезюмеОсобенность операторов инкремента и декремента состоит в том, что у них есть префиксная и постфиксная формы с немного отличающейся семантикой. Определяйте операторы operator++и operator--так, чтобы они подражали поведению своих встроенных двойников. Если только вам не требуется исходное значение — используйте префиксные версии операторов. Обсуждение Старая шутка гласит, что язык называется С++, а не ++С, потому что язык был улучшен (на что указывает инкремент), но многие продолжают использовать его как С (предыдущее значение до инкремента). К счастью, эту шутку можно считать устаревшей, но это отличная иллюстрация для понимания отличия между двумя формами операторов. В случае ++и --постфиксные формы операторов возвращают исходное значение, в то время как префиксные формы возвращают новое значение. Лучше всего реализовывать постфиксный оператор с использованием префиксного. Вот канонический вид такого использования: // ---- Префиксные операторы ----------------------- T& T::operator++() { // Префиксный вид: // выполнение // - Выполнение // инкремента // действий return *this; // - return *this; } T& T::operator--() { // Префиксный вид: // Выполнение // - Выполнение // декремента // действий return *this; // - return *this; } // ---- Постфиксные операторы --------------------- T T::operator++(int) { // Постфиксный вид: T old(*this); // - Запоминаем старое значение ++*this; // - Вызов префиксной версии return old; // - Возврат старого значения } T T::operator--(int) { // Постфиксный вид: T old(*this); // - Запоминаем старое значение --*this; // - Вызов префиксной версии return old; // - Возврат старого значения } В вызывающем коде лучше использовать префиксные операторы, если только вам не требуется исходное значение, возвращаемое постфиксной версией. Префиксная форма семантически эквивалентна, она вводится практически так же, и зачастую немного эффективнее, так как создает на один объект меньше. Это не преждевременная оптимизация, а устранение преждевременной пессимизации (см. рекомендацию 9). ИсключенияШаблоны, используемые для научных вычислений (например, шаблоны для представления тензоров или матриц), к которым предъявляются жесткие требования по производительности. Для таких шаблонов часто приходится искать более эффективные способы реализации префиксных форм операторов. Ссылки[Cline99] §23.07-08 • [Dewhurst03] §87 • [Meyers96] §6 • [Stroustrup00] §19.3 • [Sutter00] §6, §20 29. Используйте перегрузку, чтобы избежать неявного преобразования типовРезюмеНе приумножайте объекты сверх необходимости (Бритва Оккама): неявное преобразование типов обеспечивает определенное синтаксическое удобство (однако см. рекомендацию 40), но в ситуации, когда допустима оптимизация (см. рекомендацию 8) и желательно избежать создания излишних объектов, можно обеспечить перегруженные функции с сигнатурами, точно соответствующими распространенным типам аргументов, и тем самым избежать преобразования типов. ОбсуждениеКогда вы работаете в офисе и у вас заканчивается бумага, что вы делаете? Правильно — вы идете к копировальному аппарату и делаете несколько копий чистого листа бумаги. Как бы глупо это ни звучало, но зачастую это именно то, что делает неявное преобразование типов: создание излишних временных объектов только для того, чтобы выполнить некоторые тривиальные операции над ними и тут же их выбросить (см. рекомендацию 40). В качестве примера можно рассмотреть сравнение строк: class String { // ... String(const char* text); // Обеспечивает неявное // преобразование типов }; bool operator==(const String&, const String&); // ... где-то в коде ... if (someString == "Hello"){ ... } Ознакомившись с приведенными выше определениями, компилятор компилирует приведенное сравнение таким образом, как если бы оно было записано в виде someString == String("Hellо"). Это слишком расточительно — копировать символы, чтобы потом просто прочесть их. Решение этой проблемы очень простое — определить перегруженные функции, чтобы избежать преобразования типов, например: bool operator==(const String& lhs, const string& rhs); // #1 bool operator==(const String& lhs, const char* rhs); // #2 bool operator==(const char* lhs, const String& rhs); // #3 Это выглядит как дублирование кода, но на самом деле это всего лишь "дублирование сигнатур", поскольку все три варианта обычно используют одну и ту же функцию. Вряд ли вы впадете в ересь преждевременной оптимизации (см. рекомендацию 8) при такой простой перегрузке, тем более что этот метод слабо применим при проектировании библиотек, когда трудно заранее предсказать, какие именно типы будут использоваться в коде, критическом по отношению к производительности. Ссылки[Meyers96] §21 • [Stroustrup00] §11.4, §C.6 • [Sutter00] §6 30. Избегайте перегрузки Резюме |
|
||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх |
||||
|