13.8 – Перегрузка операторов инкремента и декремента
Перегрузка операторов инкремента (++
) и декремента (--
) довольно проста, за одним небольшим исключением. Фактически существует две версии операторов инкремента и декремента: префиксные инкремент и декремент (например, ++x; --y;
) и постфиксные инкремент и декремент (например, x++; y--;
).
Поскольку операторы инкремента и декремента являются унарными операторами и изменяют свои операнды, их лучше всего перегружать как функции-члены. Сначала мы рассмотрим префиксные версии, потому что они проще.
Перегрузка префиксных инкремента и декремента
Префиксные инкремент и декремент перегружается точно так же, как и любой обычный унарный оператор. Рассмотрим это на примере:
#include <iostream>
class Digit
{
private:
int m_digit;
public:
Digit(int digit=0)
: m_digit{digit}
{
}
Digit& operator++();
Digit& operator--();
friend std::ostream& operator<< (std::ostream &out, const Digit &d);
};
Digit& Digit::operator++()
{
// Если наше число уже равно 9, переходим к 0
if (m_digit == 9)
m_digit = 0;
// в противном случае просто увеличиваем значение
else
++m_digit;
return *this;
}
Digit& Digit::operator--()
{
// Если наше число уже равно 0, переходим к 9
if (m_digit == 0)
m_digit = 9;
// в противном случае просто уменьшаем значение
else
--m_digit;
return *this;
}
std::ostream& operator<< (std::ostream &out, const Digit &d)
{
out << d.m_digit;
return out;
}
int main()
{
Digit digit(8);
std::cout << digit;
std::cout << ++digit;
std::cout << ++digit;
std::cout << --digit;
std::cout << --digit;
return 0;
}
Наш класс Digit
содержит число от 0 до 9. Мы перегрузили инкремент и декремент, поэтому они увеличивают/уменьшают значение цифры m_digit
, заворачиваясь по кругу, если цифра увеличивается/уменьшается за пределы диапазона.
В этом примере печатается:
89098
Обратите внимание, что мы возвращаем *this
. Перегруженные операторы инкремента и декремента возвращают текущий неявный объект, поэтому несколько операторов могут быть «объединены» вместе.
Перегрузка постфиксных инкремента и декремента
Обычно функции могут быть перегружены, если они имеют одно и то же имя, но разное количество и/или разные типы параметров. Однако рассмотрим случай префиксных и постфиксных операторов инкремента и декремента. Оба имеют одно и то же имя (например, operator++
), являются унарными и принимают один параметр одного и того же типа. Итак, как можно отличить их при перегрузке?
Ответ заключается в том, что C++ использует «фиктивную переменную» или «фиктивный аргумент» для постфиксных операторов. Этот аргумент является фиктивным целочисленным параметром, который служит только для того, чтобы отличить постфиксную версию инкремента/декремента от префиксной версии. Вот приведенный выше класс Digit
с перегрузкой как префиксных, так и постфиксных версий:
class Digit
{
private:
int m_digit;
public:
Digit(int digit=0)
: m_digit{digit}
{
}
Digit& operator++(); // префиксный
Digit& operator--(); // префиксный
Digit operator++(int); // постфиксный
Digit operator--(int); // постфиксный
friend std::ostream& operator<< (std::ostream &out, const Digit &d);
};
Digit& Digit::operator++()
{
// Если наше число уже равно 9, переходим к 0
if (m_digit == 9)
m_digit = 0;
// в противном случае просто увеличиваем значение
else
++m_digit;
return *this;
}
Digit& Digit::operator--()
{
// Если наше число уже равно 0, переходим к 9
if (m_digit == 0)
m_digit = 9;
// в противном случае просто уменьшаем значение
else
--m_digit;
return *this;
}
Digit Digit::operator++(int)
{
// Создаем временную переменную с нашей текущей цифрой
Digit temp{*this};
// Используем префиксный оператор для увеличения этой цифры
++(*this); // применяем оператор
// возвращаем временную переменную
return temp; // возвращаем сохраненное состояние
}
Digit Digit::operator--(int)
{
// Создаем временную переменную с нашей текущей цифрой
Digit temp{*this};
// Используем префиксный оператор для уменьшения этой цифры
--(*this); // применяем оператор
// возвращаем временную переменную
return temp; // возвращаем сохраненное состояние
}
std::ostream& operator<< (std::ostream &out, const Digit &d)
{
out << d.m_digit;
return out;
}
int main()
{
Digit digit(5);
std::cout << digit;
std::cout << ++digit; // вызывает Digit::operator++();
std::cout << digit++; // вызывает Digit::operator++(int);
std::cout << digit;
std::cout << --digit; // вызывает Digit::operator--();
std::cout << digit--; // вызывает Digit::operator--(int);
std::cout << digit;
return 0;
}
Этот код печатает
5667665
Здесь происходит несколько интересных вещей. Во-первых, обратите внимание, что мы отделили префиксные операторы от постфиксных операторов, предоставив целочисленный фиктивный параметр в постфиксной версии. Во-вторых, поскольку этот фиктивный параметр не используется в реализации функции, мы даже не дали ему имени. Это указывает компилятору рассматривать эту переменную как заполнитель, что означает, что он не будет предупреждать нас о том, что мы объявили переменную, но никогда ее не использовали.
В-третьих, обратите внимание, что префиксные и постфиксные операторы выполняют одну и ту же работу – они оба инкрементируют или декрементируют объект. Разница между ними заключается в возвращаемом ими значении. Перегруженные префиксные операторы возвращают объект после того, как он был инкрементирован или декрементирован. Следовательно, перегрузить их довольно просто. Мы просто увеличиваем или уменьшаем наши переменные-члены, а затем возвращаем *this
.
Постфиксные операторы, напротив, должны возвращать состояние объекта до его инкремента или декремента. Это приводит к небольшой головоломке: если мы инкрементируем или декрементируем объект, мы не сможем вернуть состояние объекта до того, как он был инкрементирован или декрементирован. И, напротив, если мы вернем состояние объекта до того, как мы инкрементируем или декрементируем его, инкремент или декремент никогда не будет вызван.
Типовой способ решения этой проблемы – использование временной переменной, которая содержит значение объекта до его инкремента или декремента. Затем сам объект можно инкрементировать или декрементировать. И в конце вызывающему возвращается временная переменная. Таким образом, вызывающий получает копию объекта до того, как он был инкрементирован или декрементирован, но сам объект будет инкрементирован или декрементирован. Обратите внимание, что это означает, что возвращаемое значение перегруженного оператора не должно быть ссылкой, потому что мы не можем вернуть ссылку на локальную переменную, которая будет уничтожена при выходе из функции. Также обратите внимание, что это означает, что постфиксные операторы обычно менее эффективны, чем префиксные операторы из-за дополнительных накладных расходов на создание экземпляра временной переменной и возврат по значению вместо ссылки.
Наконец, обратите внимание, что мы написали пост-инкремент и пост-декремент таким образом, что они вызывают пре-инкремент и пре-декремент для выполнения большей части работы. Это сокращает дублирование кода и упрощает внесение изменений в наш класс в будущем.