Перегрузка операторов сравнения и арифметических операторов. Основные правила реализации и применения
● Глобальный оператор ==:
// Реализация глобального оператора сравнения на равенство дат
inline bool operator == ( const Date & d1, const Date & d2 )
{ return d1.GetYear() == d2.GetYear() &&
d1.GetMonth() == d2.GetMonth() &&
d1.GetDay() == d2.GetDay() ;}
Если перегружен оператор сравнения на равенство (==), логично перегрузить и противоположный по смыслу оператор сравнения на неравенство (!=). Обычно проверку на неравенство реализовывают не по аналогии с проверкой на равенство, а как отрицание результата перегруженного оператора равенства. Это позволяет изменять семантику операции сравнения на равенство в единственном месте в коде, получая обновленный оператор сравнения на неравенство без малейших усилий.
● Глобальный оператор != :
// Уже имеющийся глобальный оператор сравнения на равенство дат
inline bool operator == ( const Date & d1, const Date & d2 );
// Реализация нового глобального оператора сравнения на неравенство дат
inline bool operator != ( const Date & d1, const Date & d2 )
{ // Неравенство - это отрицание равенства
return !( d1 == d2 ); }
В приведенном ниже примере перегружаются остальные операторы сравнения ( <, <=, >, >= ). При этом, фактически полностью определяется только один их операторов - сравнение по “меньше” (<), а остальные выражаются через него и ранее определенный оператор сравнения на равенство (==). Все операторы сравнения объединяет возвращаемый логический тип.
class Date
{ // … без изменений …
public:
// Объявление еще 4 перегруженных операторов сравнения
bool operator < ( const Date & d ) const;
bool operator <= ( const Date & d ) const;
bool operator > ( const Date & d ) const;
bool operator >= ( const Date & d ) const;
};
// Реализация оператора сравнения по “меньше”
bool Date::operator < ( const Date & d ) const
{ // Если год нашей даты меньше года той даты, то наша дата меньше той
if ( m_Year < d.GetYear() )
return true;
// Если год нашей даты равен году той даты - сравниваем месяцы
else if ( m_Year == d.GetYear() )
{ // Аналогично, при равных годах, если месяц нашей даты меньше месяца той даты,то наша дата меньше той
if ( m_Month < _d.GetMonth() )
return true;
// Если и годы, и месяцы равны, то следует сравнить дни
else if ( m_Month == _d.GetMonth() )
return m_Day < _d.GetDay();
// Если при равных годах месяц нашей даты больше месяца той даты,
// то наша дата больше той ( выйдем по return false ниже )
}
// Если год нашей даты больше года той даты, наша дата больше той
return false;
}
// Реализация оператора сравнения по “больше”:
// достаточно поменять местами операнды и сравнить даты по “меньше”
bool Date::operator > ( const Date & d ) const
{ return d < * this;}
// Реализация оператора сравнения по “меньше или равно”:
// истина => когда наша дата либо меньше той даты, либо равна ей
// ложь => в остальных случаях ( когда сработает > )
bool Date::operator <= ( const Date & d ) const
{ return ( * this < d ) || ( * this == d );}
// Реализация оператора сравнения по “больше или равно”:
// истина => когда та дата меньше нашей даті, ли равна ей
// ложь => в остальных случаях ( когда сработает < )
bool Date::operator >= ( const Date & d ) const
{ return ( d < * this ) || ( * this == d );}
Перегрузка оператора сравнения по “меньше” имеет широкое применение. Например, это является необходимым условием, чтобы массивы объектов классов можно было сортировать без применения специальных объектов-компараторов, как массивы встроенных типов. Оператор “<” также пригодится для реализации бинарного дерева поиска (BST).
Перегрузка арифметических операторов, таких как сложение, вычитание, умножение, деление - часто востребована в задачах, оперирующих понятиями из математики, физики и других наук с тем или иным формализованным алгебраическим аппаратом. Пожалуй, применение перегрузки для арифметических операторов математических абстракций является наиболее естественным применением перегрузки операторов в принципе - комплексные числа, матрицы, геометрические объекты, и т.п.
Обычно, арифметические операторы перегружаются связанными парами:
● немодифицирующий оператор, принимающий два объекта-операнда, возвращающий новый результат (новый объект или число, в зависимости от смысла оператора)
● модифицирующий оператор, принимающий два объекта и формирующий итоговый результат в первом объекте (возвращает ссылку на себя).
Например, если перегружается оператор немодифицирующий сложения “+”, то перегружают и парный ему модифицирующий оператор “+=” и т.д. Обычно определяют реализацию для одного из них в виде какого-либо алгоритма, а парный оператор реализовывают на основе вызова первого. Какой именно из операторов пары брать за основу не имеет принципиального значения.
В случае необходимости оператор может быть перегружен в нескольких версиях, если типы аргументов отличаются между собой. Это реализуется при помощи механизма перегрузки функций. В данном случае в С++ имеется терминологический конфликт - перегрузка перегруженных операторов (overloading overloaded operators). Первое слово “перегрузка” означает наличие функций, обладающих одинаковым названием, но различными типами/количеством аргументов. Второй слово “перегруженных” означает обсуждаемую в данной лекции перегрузку операторов.
15. Перегрузка операторов индексной выборки, префиксного и постфиксного инкремента/декремента. Перегрузка операторов преобразования типа.
Для операторов разыменования и индексной выборки необходимо предоставлять 2 версии - константную и неконстантную, поскольку такие операторы могут быть использованы как в правой так и в левой части присвоения. Константная версия возвращает значение в массиве. Неконстантная - ссылку на место его хранения.
// Оператор разыменования с целью чтения значения из указателя
intIntegerArrayPtr::operator * () const
{
// Проверка валидности - вызов преобразования к типу bool
assert( * this);
// Доступ к значению - элементу массива
returnm_pData[ m_currentPosition ];
}
//*******************************************************************************
// Оператор разыменования с целью записи значения через указатель
int& IntegerArrayPtr::operator* ()
{
// Проверка валидности - вызов преобразования к типу bool
assert( * this);
// Возврат ссылки на интересующую ячейку массива
returnm_pData[ m_currentPosition ];
}
Оператор индексной выборки с целью чтения значения
int IntegerArrayPtr::operator[] ( int_index ) const
{
// Проверка валидности - вызов преобразования к типу bool
// Проверка корректности переданного индекса
assert( * this&& ( m_currentPosition + _index ) < m_length );
// Доступ к значению - элементу массива
returnm_pData[ m_currentPosition + _index ];
}
//*******************************************************************************
// Оператор индексной выборки с целью записи значения
int& IntegerArrayPtr::operator[] ( int_index )
{
// Проверка валидности - вызов преобразования к типу bool
// Проверка корректности переданного индекса
assert( * this&& ( m_currentPosition + _index ) < m_length );
// Возврат ссылки на интересующую ячейку массива
returnm_pData[ m_currentPosition + _index ];
}
Префиксные операторы инкремента/декремента всегда возвращает ссылку на себя. Сначала объект модифицируется, а затем идет возврат ссылки, поскольку изменение должно вступить в силу и сразу использоваться в дальнейшем выражении.
Постфиксные операторы инкремента/декремента всегда возвращают новый объект по значению. Сначала создается копия текущего состояния объекта. Затем сам объект модифицируются. А возвращается ранее созданная копия. Таким образом сначала используется старое значение, а затем происходит его изменение.
С точки зрения синтаксиса перегрузки, отличить префиксные операторы от постфиксных можно по фиктивному аргументу типа int. Такой аргумент фактически не передается, его нельзя использовать при реализации. Этот неиспользуемый аргумент является лишь синтаксическим средством-обманом, позволяющим отличать перегружаемые префиксные и постфиксные операторы друг от друга
// Префиксный оператор инкремента
IntegerArrayPtr& IntegerArrayPtr::operator++ ()
{
// Проверяем валидность
assert( * this);
// Увеличиваем позицию на 1
++ m_currentPosition;
// Используем новое состояния сразу, возвращаем ссылку на себя
return* this;
}
// Постфиксный оператор инкремента - с фиктивным аргументом
IntegerArrayPtr IntegerArrayPtr::operator ++ ( int)
{
// Проверяем валидность
assert( * this);
// Создаем объект, копируем в него текущее состояние
IntegerArrayPtr copy( * this);
// Увеличиваем позицию на 1
++ m_currentPosition;
// Возвращаем копию по значению
returncopy;
}
// Префиксный оператор декремента
IntegerArrayPtr& IntegerArrayPtr::operator-- ()
{
// Проверяем валидность
assert( * this);
// Уменьшаем позицию на 1
--m_currentPosition;
// Используем новое состояния сразу, возвращаем ссылку на себя
return* this;
}
//*******************************************************************************
// Постфиксный оператор декремента - с фиктивным аргументом
IntegerArrayPtr IntegerArrayPtr::operator-- ( int)
{
// Проверяем валидность
assert( * this);
// Создаем объект, копируем в него текущее состояние
IntegerArrayPtr copy( * this);
// Уменьшаем позицию на 1
-- m_currentPosition;
// Возвращаем копию по значению
returncopy;
}
Перегрузка операторов преобразования типа:
Если тип первого операнда является классом, существует 2 альтернативы для перегрузки операторов:
● реализация перегруженного оператора внутри класса в виде функции-члена:
○ для унарных операций единственным операндом является объект, на который указывает неявно передаваемый указатель this;
○ для бинарных - неявно передаваемый указатель this указывает на левый операнд, а правый операнд передается в качестве аргумента оператора;
Операторы преобразования типа могут быть реализованы только таким способом, второй вариант не доступен.
// Оператор преобразования к логическому типу:
// истина => если указатель указывает на валидную позицию в массиве
// ложь => если позиция вызодит за допустимую границу
IntegerArrayPtr::operator bool() const
{
returnm_currentPosition < m_length;
}