Лекция 7. Перегрузка операторов

Что такое перегрузка?

Перегрузка - это возможность поддерживать несколько функций с одним названием, но разными сигнатурами вызова. Рассмотрим пример:

double sqrt ( double x ); //Функция корня для чисел с плавающей точкой
int sqrt ( int x ); //Функция корня для целых чисел
...
sqrt(1.5);//В этом случае вызовется функция чисел с плавающей точкой
sqrt(7);//А в этом уже для целых чисел

Компилятор сам определяет какую функцию выбрать в зависимости от сигнатуры. Более подробно о том, как компилятор определяет какую из перегруженных функций выбрать, можно прочесть в стандарте языка С++.

 
Что такое операторы?

Пример операторов:

+, <<, & и т.д.

Оператор в С++ - это некоторое действие или функция обозначенная специальным символом. Для того что бы распространять эти действия на новые типы данных, при этом сохраняя естественный синтаксис, в С++ была введена возможность перегрузки операторов.

Список операторов:

+ - * / % //Арифметические операторы
+= -= *= /= %=
+a -a //Операторы знака
++a a++ --a a-- //Префиксный и постфиксный инкременты
&& || ! //Логические операторы
& | ~ ^
&= |= ^=
<< >> <<= >>= //Битовый сдвиг
= //Оператор присваивания
== != //Операторы сравнения
< > >= <=

Специальные операторы:

&a *a a-> a->*
() []
(type)
. ,  (a ? b : c)

Не все операторы можно переопределять. Операторы "." и "a?b:c"(тернарный оператор) переопределить нельзя.

Так же нужно отметить, что переопределяя операторы "," "&&" "||" теряются их "ленивые" свойства.

Операторы "a->", "[]", "()", "=" и "(type)" можно переопределить только как методы класса.

 
Как переопределить оператор?

Рассмотрим как переопределять операторы на примере нашего класса длинной арифметики BigInt.

Бинарный оператор

BigInt operator+(BigInt const & a, BigInt const & b)
{
	...
}

Бинарный оператор - это функция от двух параметров, параметрами которой являются левый и правый операнды оператора.

Унарный оператор

BigInt & operator++(BigInt const & b)
{
	...
}

Унарный оператор - это оператор от одного параметра. Если он объявлен внутри класса, то этим параметром (неявным) является this.

При перегрузке операторов ">>" и "<<", для ввода и вывода через потоки нужно подключить заголовочный файл iostream.

#include <iostream>
...
std::istream & operator >>(std::istream & is, BigInt & n)
{
	...
}
std::ostream & operator <<(std::ostream & os, BigInt const & n)
{
	...
}

Переопределение префиксных и постфиксных операторов:

BigInt & operator --(BigInt & n);//Префиксный
BigInt operator --(BigInt & n, int);//Постфиксный

Фактически параметра size_t size у операции нет — он фиктивен. Это хак для того чтобы внести различия в сигнатуры

Переопределение операторов внутри класса:

class BigInt
{
	...
	BigInt * operator ->();
	char operator [](size_t i) const;
	char & operator [](size_t i);
	...
};

Обратите внимание, что при переопределении "->" необходимо вернуть именно указатель на объект.

Как вы могли заметить, оператор "[]" переопределен как константный и не константный. Во втором случае мы возвращаем ссылку на объект, а не сам объект. Это может быть полезно при определении массивов.

 
Как правильно перегружать оператор "=" ?

Для перегрузки оператора "=" есть специальная идиома, которая облегчает присваивание сложных объектов.
Рассмотрим следующий код:

class BigInt
{
	size_t size_;
	char * digits_;

	BigInt(BigInt const & num)
    {
      ...
    }
	
	void swap(BigInt & b)
	{
		std::swap(size_, b.size_);
		std::swap(digits_, b.digits_);
	}
	BigInt & operator = (BigInt const & num)
	{
		if(this != &n)
		{
			BigInt(num).swap(*this);
		}
		return *this;
	} 
	...
};

Функция std::swap(a,b) - меняет значение a и b местами.

Создав временный объект равный num, поменяем его значения с текущими значениями объекта *this. Выйдя из функции временный объект удалится, а в *this останутся новые значения.

 
Приведение типов

Часто при разработке новых классов появляется желание приводить уже существующие типы к новому, и наоборот.

B b;
A a(b);

Оператор приведения - это противоположность конструктору с одним параметром.

class BigInt
{
	operator string() const;
	{
		string s;
		...
		return s;
	}
};

Важно понять, что в случае

int b;
A a = b;		

и во всех подобных случаях, когда необходимо одно значение привести к другому и оператор приведения не определен, то будет вызываться конструктор копирования A::A(int b) от соответствующего типа. Если определить и оператор приведения, и конструктор копирования как не explicit, то при вызове произойдет ошибка компиляции.

 
Как переопределить пост- и пре- фиксные операторы?

Хорошим тоном является определение постфиксного оператора через префиксный:

BigInt & operator ++()
{
	...
	return *this; 
}
BigInt & operator ++( size_t )
{
	BigInt t(*this);
	++(*this);
	return t; 
}
(В этом примере операторы определялись внутри класса BigInt.)

 
Операторы сравнения

При определении операторов сравнения нет смысла определять их все. Проще определить операторы > или < и ==. А через них уже определить все остальные.

bool operator ==(BigInt const & a, BigInt const & b)
{
	...
	return ...;
}
bool operator !=(BigInt const & a, BigInt const & b)
{
	return !( a == b);
}
bool operator <(BigInt const & a, BigInt const & b)
{
	...
	return ...;
}
bool operator >(BigInt const & a, BigInt const & b)
{
	return b < a;
}

bool operator <=(BigInt const & a, BigInt const & b)
{
	return !(a > b);
}

bool operator >=(BigInt const & a, BigInt const & b)
{
	return !(a < b);
}

Операторы сравнения лучше определять вне класса. Рассмотрим следующий код:

BigInt a(3);
BigInt b(2);
a < b //выполнится в обоих случаях
a < 2 //Так же выполнится в обоих случаях. "2" приведется к BigInt
3 < b //!!! Будет работать только если оператор сравнения определен вне класса. 
      //Т.к. нету приведения BigInt к "3" 

 
Несколько замечаний под конец

  1. Унарные операторы(+=, -=, *= и т.д.) лучше делать внутри класса. А бинарные операторы(+, -, * и т.д.) на их основе, но уже снаружи класса.
  2. Быть осторожнее с explicit конструкторами.
  3. Нельзя определять новые операторы и нельзя менять приоритет операторов, т.к. система парсинга языка С++ этого не позволяет.