Оператора с побочным действием

Обновл. 30 Сен 2020 |
Из урока №102 мы уже знаем, что перегрузка функций обеспечивает механизм создания и выполнения вызовов функций с одним и тем же именем, но с разными параметрами. Это позволяет одной функции работать с несколькими разными типами данных (без необходимости придумывать уникальные имена для каждой из функций).
В языке C++ операторы реализованы в виде функций. Используя перегрузку функции оператора, вы можете определить свои собственные версии операторов, которые будут работать с разными типами данных (включая классы). Использование перегрузки функции для перегрузки оператора называется перегрузкой оператора.
Операторы, как функции
Рассмотрим следующий фрагмент:
int a = 5; int b = 6; std::cout << a + b << ‘n’; |
Здесь компилятор использует встроенную версию оператора плюс (+) для целочисленных операндов — эта функция сложит два целочисленных значения (a и b), и возвратит целочисленный результат. Когда вы видите выражение a + b, то думайте о нем, как о вызове функции operator+(a, b) (где operator+ является именем функции).
Теперь рассмотрим следующий фрагмент:
double m = 4.0; double p = 5.0; std::cout << m + p << ‘n’; |
Компилятор также предоставит встроенную версию оператора плюс (+) для операндов типа double. Выражение m + p приведет к вызову функции operator+(m, p), а, благодаря перегрузке оператора, вызовется версия double (вместо версии int).
Теперь рассмотрим, что произойдет, если мы попытаемся добавить два объекта класса:
Mystring hello = “Hello, “; Mystring world = “World!”; std::cout << hello + world << ‘n’; |
Как вы думаете, какой будет результат? Наверное, вывод строки Hello, World!? Нет, результатом будет ошибка, так как класс Mystring является пользовательским типом данных, а компилятор не имеет встроенной версии operator+() для использования с операндами Mystring. Для того, чтобы сделать то, что мы хотим, нам придется написать свою версию функции operator+() и указать в ней алгоритм работы с операндами типа Mystring. То, как это сделать в коде, мы рассмотрим на следующем уроке.
Вызов перегруженных операторов
При обработке выражения, содержащего оператор, компилятор использует следующие алгоритмы действий:
Если все операнды являются фундаментальных типов данных, то вызывать следует встроенные соответствующие версии операторов (если таковые существуют). Если таковых не существует, то компилятор выдаст ошибку.
Если какой-либо из операндов является пользовательского типа данных (например, объект класса или типа перечисления), то компилятор будет искать версию оператора, которая работает с таким типом данных. Если компилятор не найдет ничего подходящего, то попытается выполнить конвертацию одного или нескольких операндов пользовательского типа данных в фундаментальные типы данных, чтобы таким образом он мог использовать соответствующий встроенный оператор. Если это не сработает — компилятор выдаст ошибку.
Ограничения в перегрузке операторов
Во-первых, почти любой существующий оператор в языке C++ может быть перегружен. Исключениями являются:
тернарный оператор (?:);
оператор sizeof;
оператор разрешения области видимости (::);
оператор выбора члена (.);
указатель, как оператор выбора члена (.*).
Во-вторых, вы можете перегрузить только существующие операторы. Вы не можете создавать новые или переименовывать существующие. Например, вы не можете создать оператор ** для выполнения операции возведения в степень.
В-третьих, по крайней мере один из операндов перегруженного оператора должен быть пользовательского типа данных. Это означает, что вы не можете перегрузить operator+() для выполнения операции сложения значения типа int со значением типа double. Однако вы можете перегрузить operator+() для выполнения операции сложения значения типа int с объектом класса Mystring.
В-четвертых, изначальное количество операндов, поддерживаемых оператором, изменить невозможно. Т.е. с бинарным оператором используются только два операнда, с унарным — только один, с тернарным — только три.
Наконец, все операторы сохраняют свой приоритет и ассоциативность по умолчанию (независимо от того, для чего они используются), и это не может быть изменено.
Некоторые начинающие программисты пытаются перегрузить побитовый оператор XOR (^) для выполнения операции возведения в степень. Однако в языке C++ у оператора ^ приоритет ниже, чем у базовых арифметических операторов (+, -, *, /), и это приведет к некорректной обработке выражений.
В математике операция возведения в степень выполняется до выполнения базовых арифметических операций, поэтому 2 + 5 ^ 2 обрабатывается как 2 + (5 ^ 2) => 2 + 25 => 27. Однако в языке C++ у базовых арифметических операторов приоритет выше, нежели у оператора ^, поэтому 2 + 5 ^ 2 выполнится как (2 + 5) ^ 2 => 7 ^ 2 => 49.
Вам нужно будет явно заключать в скобки часть с возведением в степень (например, 2 + (5 ^ 2)) каждый раз, когда вы хотите, чтобы она выполнялась первой, что очень легко забыть и, таким образом, наделать ошибок. Поэтому проводить подобные эксперименты не рекомендуется.
Примечание: В языке C++ для возведения в степень используется функция pow() из заголовочного файла cmath. В примере, приведенном выше, с выполнением выражения 2 + 5 ^ 2 в языке C++, имеется в виду, что вы перегрузите побитовый оператор XOR (^) для выполнения операции возведения в степень.
Правило: При перегрузке операторов старайтесь максимально приближенно сохранять функционал операторов в соответствии с их первоначальными применениями.
Для чего использовать перегрузку операторов? Вы можете перегрузить оператор + для соединения объектов вашего класса String или для выполнения операции сложения двух объектов вашего класса Fraction. Вы можете перегрузить оператор << для вывода вашего класса на экран (или записи в файл). Вы можете перегрузить оператор равенства (==) для сравнения двух объектов класса и т.д. Подобные применения делают перегрузку операторов одной из самых полезных особенностей в языке C++, так как это упрощает процесс работы с классами и открывает новые возможности.
Оценить статью:
Загрузка…
Источник
Доброго времени суток!
Желание написать данную статью появилось после прочтения поста Перегрузка C++ операторов, потому что в нём не были раскрыты многие важные темы.
Самое главное, что необходимо помнить — перегрузка операторов, это всего лишь более удобный способ вызова функций, поэтому не стоит увлекаться перегрузкой операторов. Использовать её следует только тогда, когда это упростит написание кода. Но, не настолько, чтобы это затрудняло чтение. Ведь, как известно, код читается намного чаще, чем пишется. И не забывайте, что вам никогда не дадут перегрузить операторы в тандеме со встроенными типами, возможность перегрузки есть только для пользовательских типов/классов.
Синтаксис перегрузки
Синтаксис перегрузки операторов очень похож на определение функции с именем operator@, где @ — это идентификатор оператора (например +, -, <<, >>). Рассмотрим простейший пример:
class Integer
{
private:
int value;
public:
Integer(int i): value(i)
{}
const Integer operator+(const Integer& rv) const {
return (value + rv.value);
}
};
В данном случае, оператор оформлен как член класса, аргумент определяет значение, находящееся в правой части оператора. Вообще, существует два основных способа перегрузки операторов: глобальные функции, дружественные для класса, или подставляемые функции самого класса. Какой способ, для какого оператора лучше, рассмотрим в конце топика.
В большинстве случаев, операторы (кроме условных) возвращают объект, или ссылку на тип, к которому относятся его аргументы (если типы разные, то вы сами решаете как интерпретировать результат вычисления оператора).
Перегрузка унарных операторов
Рассмотрим примеры перегрузки унарных операторов для определенного выше класса Integer. Заодно определим их в виде дружественных функций и рассмотрим операторы декремента и инкремента:
class Integer
{
private:
int value;
public:
Integer(int i): value(i)
{}
//унарный +
friend const Integer& operator+(const Integer& i);
//унарный –
friend const Integer operator-(const Integer& i);
//префиксный инкремент
friend const Integer& operator++(Integer& i);
//постфиксный инкремент
friend const Integer operator++(Integer& i, int);
//префиксный декремент
friend const Integer& operator–(Integer& i);
//постфиксный декремент
friend const Integer operator–(Integer& i, int);
};
//унарный плюс ничего не делает.
const Integer& operator+(const Integer& i) {
return i.value;
}
const Integer operator-(const Integer& i) {
return Integer(-i.value);
}
//префиксная версия возвращает значение после инкремента
const Integer& operator++(Integer& i) {
i.value++;
return i;
}
//постфиксная версия возвращает значение до инкремента
const Integer operator++(Integer& i, int) {
Integer oldValue(i.value);
i.value++;
return oldValue;
}
//префиксная версия возвращает значение после декремента
const Integer& operator–(Integer& i) {
i.value–;
return i;
}
//постфиксная версия возвращает значение до декремента
const Integer operator–(Integer& i, int) {
Integer oldValue(i.value);
i.value–;
return oldValue;
}
Теперь вы знаете, как компилятор различает префиксные и постфиксные версии декремента и инкремента. В случае, когда он видит выражение ++i, то вызывается функция operator++(a). Если же он видит i++, то вызывается operator++(a, int). То есть вызывается перегруженная функция operator++, и именно для этого используется фиктивный параметр int в постфиксной версии.
Бинарные операторы
Рассмотрим синтаксис перегрузки бинарных операторов. Перегрузим один оператор, который возвращает l-значение, один условный оператор и один оператор, создающий новое значение (определим их глобально):
class Integer
{
private:
int value;
public:
Integer(int i): value(i)
{}
friend const Integer operator+(const Integer& left, const Integer& right);
friend Integer& operator+=(Integer& left, const Integer& right);
friend bool operator==(const Integer& left, const Integer& right);
};
const Integer operator+(const Integer& left, const Integer& right) {
return Integer(left.value + right.value);
}
Integer& operator+=(Integer& left, const Integer& right) {
left.value += right.value;
return left;
}
bool operator==(const Integer& left, const Integer& right) {
return left.value == right.value;
}
Во всех этих примерах операторы перегружаются для одного типа, однако, это необязательно. Можно, к примеру, перегрузить сложение нашего типа Integer и определенного по его подобию Float.
Аргументы и возвращаемые значения
Как можно было заметить, в примерах используются различные способы передачи аргументов в функции и возвращения значений операторов.
- Если аргумент не изменяется оператором, в случае, например унарного плюса, его нужно передавать как ссылку на константу. Вообще, это справедливо для почти всех арифметических операторов (сложение, вычитание, умножение…)
- Тип возвращаемого значения зависит от сути оператора. Если оператор должен возвращать новое значение, то необходимо создавать новый объект (как в случае бинарного плюса). Если вы хотите запретить изменение объекта как l-value, то нужно возвращать его константным.
- Для операторов присваивания необходимо возвращать ссылку на измененный элемент. Также, если вы хотите использовать оператор присваивания в конструкциях вида (x=y).f(), где функция f() вызывается для для переменной x, после присваивания ей y, то не возвращайте ссылку на константу, возвращайте просто ссылку.
- Логические операторы должны возвращать в худшем случае int, а в лучшем bool.
Оптимизация возвращаемого значения
При создании новых объектов и возвращении их из функции следует использовать запись как для вышеописанного примера оператора бинарного плюса.
return Integer(left.value + right.value);
Честно говоря, не знаю, какая ситуация актуальна для C++11, все рассуждения далее справедливы для C++98.
На первый взгляд, это похоже на синтаксис создания временного объекта, то есть как будто бы нет разницы между кодом выше и этим:
Integer temp(left.value + right.value);
return temp;
Но на самом деле, в этом случае произойдет вызов конструктора в первой строке, далее вызов конструктора копирования, который скопирует объект, а далее, при раскрутке стека вызовется деструктор. При использовании первой записи компилятор изначально создаёт объект в памяти, в которую нужно его скопировать, таким образом экономится вызов конструктора копирования и деструктора.
Особые операторы
В C++ есть операторы, обладающие специфическим синтаксисом и способом перегрузки. Например оператор индексирования []. Он всегда определяется как член класса и, так как подразумевается поведение индексируемого объекта как массива, то ему следует возвращать ссылку.
Оператор запятая
В число «особых» операторов входит также оператор запятая. Он вызывается для объектов, рядом с которыми поставлена запятая (но он не вызывается в списках аргументов функций). Придумать осмысленный пример использования этого оператора не так-то просто. Хабраюзер AxisPod в комментариях к предыдущей статье о перегрузке рассказал об одном.
Оператор разыменования указателя
Перегрузка этих операторов может быть оправдана для классов умных указателей. Этот оператор обязательно определяется как функция класса, причём на него накладываются некоторые ограничения: он должен возвращать либо объект (или ссылку), либо указатель, позволяющий обратиться к объекту.
Оператор присваивания
Оператор присваивания обязательно определяется в виде функции класса, потому что он неразрывно связан с объектом, находящимся слева от “=”. Определение оператора присваивания в глобальном виде сделало бы возможным переопределение стандартного поведения оператора “=”. Пример:
class Integer
{
private:
int value;
public:
Integer(int i): value(i)
{}
Integer& operator=(const Integer& right) {
//проверка на самоприсваивание
if (this == &right) {
return *this;
}
value = right.value;
return *this;
}
};
Как можно заметить, в начале функции производится проверка на самоприсваивание. Вообще, в данном случае самоприсваивание безвредно, но ситуация не всегда такая простая. Например, если объект большой, можно потратить много времени на ненужное копирование, или при работе с указателями.
Неперегружаемые операторы
Некоторые операторы в C++ не перегружаются в принципе. По всей видимости, это сделано из соображений безопасности.
- Оператор выбора члена класса “.”.
- Оператор разыменования указателя на член класса “.*”
- В С++ отсутствует оператор возведения в степень (как в Fortran) “**”.
- Запрещено определять свои операторы (возможны проблемы с определением приоритетов).
- Нельзя изменять приоритеты операторов
Рекомендации к форме определения операторов
Как мы уже выяснили, существует два способа операторов — в виде функции класса и в виде дружественной глобальной функции.
Роб Мюррей, в своей книге C++ Strategies and Tactics определил следующие рекомендации по выбору формы оператора:
Оператор | Рекомендуемая форма |
Все унарные операторы | Член класса |
= () [] -> ->* | Обязательно член класса |
+= -= /= *= ^= &= |= %= >>= <<= | Член класса |
Остальные бинарные операторы | Не член класса |
Почему так? Во-первых, на некоторые операторы изначально наложено ограничение. Вообще, если семантически нет разницы как определять оператор, то лучше его оформить в виде функции класса, чтобы подчеркнуть связь, плюс помимо этого функция будет подставляемой (inline). К тому же, иногда может возникнуть потребность в том, чтобы представить левосторонний операнд объектом другого класса. Наверное, самый яркий пример — переопределение << и >> для потоков ввода/вывода.
Литература
Брюс Эккель — Философия C++. Введение в стандартный C++.
Источник
Побочные эффекты
Побочный эффект выражается в неявном изменении значения переменной в процессе вычисления выражения. Все операции присваивания могут вызывать побочный эффект. Вызов функции, в которой изменяется значение какой-либо внешней переменной, либо путем явного присваивания, либо через указатель, также имеет побочный эффект.
Порядок вычисления выражения зависит от реализации компилятора, за исключением случаев, в которых явно гарантируется определенный порядок вычислений (см. раздел 4.5). При вычислении выражения в языке Си существуют так называемые контрольные точки. По достижении контрольной точки все предшествующие вычисления, в том числе все побочные эффекты, гарантированно произведены. Контрольными точками являются операция последовательного вычисления, условная операция, логические операции И и ИЛИ, вызов функции. Другие контрольные точки:
—конец полного выражения (т.е. выражения, которое не является частью другого выражения);
—конец инициализирующего выражения для переменной класса памяти auto;
—конец выражений, управляющих выполнением операторов if, switch, for, do, while и выражения в операторе return. Приведем примеры побочных эффектов:
add(i + 1, i = j + 2);
Аргументы вызова функции add могут быть вычислены в любом порядке. Выражение i+1 может быть вычислено перед выражением i=j+2, или после него, с различным результатом в каждом случае.
Унарные операции инкремента и декремента также содержат в себе присваивание и могут быть причиной побочных эффектов, как это показано в следующем примере:
int i, а [10];
i = 0;
a[i++] = i;
Неизвестно, какое значение будет присвоено элементу а[0] — нуль или единица, поскольку для операции присваивания порядок вычисления аргументов не оговаривается.
Следующая глава >
Похожие главы из других книг:
10. Трюки и эффекты
На вопрос: «Какую операционную систему вы считаете самым сильным конкурентом Windows Vista?» представители Microsoft достаточно самоуверенно ответили: «Windows XP SP2».Каждая новая версия Windows создает ажиотаж во всех сферах информационных технологий. Техническая
Часть III. ЭФФЕКТЫ
В этой части мы будем говорить об эффектах (они же стили). На первый взгляд кажется, что название новое, и мы с ним еще не встречались, но это не так. Достаточно вспомнить такие эффекты, как Bevel and Emboss (Фаска и рельеф) и Gradient Overlay (Наложение градиента). Но если
Специальные эффекты
Adobe InDesign также умеет создавать интересные и полезные эффекты, которые мы можем использовать при оформлении текста. По сути, здесь мы снова сталкиваемся с взаимопроникновением программ фирмы Adobe, поскольку данные эффекты «заимствованы» из программы
Оптические эффекты
Оптические эффекты выступают как часть процесса визуализации, позволяя повысить реалистичность трехмерных сцен. Доступ к группе оптических эффектов можно получить при помощи вкладки Effects (Эффекты) окна Environment and Effects (Окружающая среда и эффекты) (рис.
Визуальные эффекты
Начнем с самого простого. Выполните команду Пуск ? Панель управления, в открывшемся окне дважды щелкните на значке Система и перейдите на вкладку Дополнительно. В области Быстродействие нажмите кнопку Параметры. В открывшемся окне Параметры
Глава 8
Эффекты
Эффекты – это специальные средства, позволяющие преобразовать некоторые элементы изображения, они могут применяться в отношении отдельных элементов, а также в отношении слоев целиком. Эффекты дают возможность зрительно выделять контуры элемента,
Стили и эффекты
Этим возможности программы Excel по работе с графическими изображениями не ограничиваются. Вы можете придать рисунку особый стиль с помощью библиотеки готовых стилей и эффектов, а также изменить геометрическую форму рисунка.Сначала изменим геометрическую
Эффекты анимации
Если бы мы работали в Word или Publisher, то на этом, собственно, пришлось бы и остановиться – что можно сделать с неподвижной картинкой? Но слайду в PowerPoint совершенно нет нужды быть неподвижным! Напротив, ему это в корне противопоказано.Надписи, картинки и прочие
Эффекты изменений
Однажды вызванный запрос к триггеру или хранимой процедуре сохраняется в кэше метаданных, пока существуют клиентские соединения с базой данных, независимо от того, использует ли какой-нибудь клиент этот триггер или хранимую процедуру. Не существует
Неожиданные эффекты
SQL позволяет.одному и тому же получателю прав получать одни и те же полномочия из различных источников, даже если предоставляемые права уже есть у получателя. Каждый раз, когда один пользователь расширяет у другого пользователя права передавать
Звуковые эффекты
Неплохо бы добавить в игру звуковые эффекты. К сожалению, библиотека .NET Compact Framework пока не поддерживает воспроизведение звуковых файлов при помощи управляемого кода. Поэтому придется воспользоваться механизмом Platform Invoke (P/Invoke). В главе, посвященной
Введение в эффекты
В программе доступно более 40 специальных эффектов и преобразователей звука. Все эффекты можно разделить на группы.• Эффекты эха – создают эффекты, добавляющие эхо.• Эффекты высоты тона – создают эффекты звучания, основанные на изменении высоты
Звуковые эффекты
В программе Studio также реализовано несколько звуковых эффектов, некоторые из которых могут оказаться весьма полезными. Для применения эффектов к выделенному аудиоклипу используется инструмент Добавление аудиоэффектов (последний в
7.4. Побочные электромагнитные излучения
Говоря о многоуровневой защите, нельзя не упомянуть о таком явлении, как утечка информации посредством паразитного электромагнитного излучения.Представьте себе такую ситуацию: ваша компания оперирует с информацией закрытого
Простейшие эффекты
Начнем мы с создания самых простых эффектов. Всего их два: клонирование выделенного экземпляра по ячейкам воображаемой сетки и его анимированное
Звуковые эффекты
Звуковые эффекты добавляют звучанию особый колорит, а иногда меняют звук до неузнаваемости.Задержка сигналовК эффектам, основанным на задержке сигнала, относятся следующие:• дилэй (от англ. delay – задержка);• реверберация (от англ. reverberation –
Источник