Skip to content

Instantly share code, notes, and snippets.

@AmatanHead
Last active December 12, 2015 10:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AmatanHead/b35a81d726c8a3da8b57 to your computer and use it in GitHub Desktop.
Save AmatanHead/b35a81d726c8a3da8b57 to your computer and use it in GitHub Desktop.
Operator overloading

Перегрузка операторов в C++

Небольшое пособие по перегрузке операторов в C++.

Для примера рассмотрим класс

class Example {
    std::vector<int> values;
public:
    // Методы
}

Конструкторы

Конструкторы по умолчанию

Не забывайте создавать конструкторы копирования и конструкторы по умолчанию.

Если у класса нет ни одного конструктора, конструктор копирования и конструктор по умолчанию будут созданы автоматически. Однако если вы добавите хотя бы один конструктор, два вышеперечисленных автоматически не создадутся.

Списки инициализации

Два примера:

Example(const vector<T>&data) { values = data; }

и

Example(const vector<T>&data): values(data) {}

В первом примере, перед тем, как зайти в тело конструктора, компилятор вызовет конструктор по умолчанию для values. После этого произойдет копирование data в values. При этом совершается лишнее конструирование по умолчанию. Второй пример лишен этого недостатка. Помимо всего прочего, список инициализации — единственный способ инициализировать константы, ссылки и объекты базовых классов. Однако и в простых ситуациях не стоит им пренебрегать.

Сигнатуры методов и константы в них

Константные методы

Прежде всего, очень часто встречающаяся ошибка — константные методы не отмечены, как const. Если ваш метод не меняет состояние класса, его обязательно нужно сделать константным.

Во-первых, это предотвращает возможные ошибки. К примеру, если вы пишете оператор ==:

bool operator == (Example &other) {
    return values = other.values;
}

Уже видите ошибку? Вместо того, чтобы сравнить values == other.values, мы делаем присвоение. И компилятор ничего нам не возразит, можно вполне и не заметить.

Когда же мы сделаем метод константным

bool operator == (Example &other) const {
    return values = other.values;
}

тут же получим ошибку при компиляции.

Во-вторых, неконстантные методы нельзя вызвать на константных объектах.

К примеру:

const Example a {1, 2};
Example b {2, 3};
if (a == b)  // ну и зачем нам константы, которые нельзя сравнить?
	...

или

std::vector<Example> classes;
...
/* std::sort не сможет вызвать метод `<`, если тот не константен,
   так как она работает со своим входом, как с массивом констант, т. е. не изменяет его.
 */
std::sort(classes.begin(), classes.end());

Передача ссылок и константных ссылок

Нужно помнить, что при передаче параметров по значению, они копируются. То есть если у вас есть метод bool operator == (Example other) const;, мы всегда будем копировать other перед тем, как передать его в operator ==.

При передаче же по ссылке, копирований не происходит, поэтому bool operator == (Example &other) const; предпочтительнее в данном случае.

С другой стороны, если метод принимает значение по ссылке, изменения этого значения внутри метода распространится на всю программу, чего не происходит, если мы изменяем копию объекта (это же копия!).

По этой причине нужно помнить про то, что аргументы, принимаемые по ссылкам, тоже можно (и нужно, если они не должны меняться)!

Рассмотрим тот же метод и чуть измененный пример:

const Example a {1, 2}, b {2, 3};  // обе переменные константны
if (a == b)
	...

Несмотря на то, что мы сделали метод константным, сравнение все еще не работает. Потому что теперь b — тоже константа. Однако в сигнатуре сказано: «я принимаю b по ссылке и могу его менять».

bool operator == (const Example &other) const {  // справедливость восстановлена!
    return values = other.values;
}

Передача указателей

Те же правила распространяются и на указатели.

Единственная разница в том, что указатель — сущность двойственная. А именно, сам по себе указатель — это переменная, в которой записан адрес некоторого объекта. Таким образом константность бывает разная:

  • int * - указатель на int
  • int const * - указатель на константный int
  • int * const - константный указатель на int
  • int const * const - константный указатель на константный int

Возвращаемое значение

Тут есть два момента, которые нужно осознать.

1. Ссылки и константы

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

Ссылку нужно возвращать в случае, когда копирование затратно, или когда возвращаемый элемент можно изменять, и изменение имеет смысл.

К примеру:

const int &operator [] (size_t index) const {
    return values[i];
}

int &operator [] (size_t index) {
    return values[i];
}

Теперь можно написать:

Example a {2, 3};
a[0] = 5;

При этом обратите внимание, что реализовано два метода: один вызывается для констант, и из него возвращается константная ссылка, а другой вызывается для обычных объектов, и ссылка обычная.

Это важно, потому что в случае, когда объект константен, мы получим ошибку — константную ссылку менять нельзя:

const Example a {2, 3};
a[0] = 5;  // error

При этом пример сверху сработал бы, если бы мы ограничились одним методом, к примеру, таким:

int &operator [] (size_t index) const {
    return values[i];
}

Кроме того, возвращение должно быть константным, когда возвращается lvalue (бинарные и унарные операторы):

const Example &operator + (const Example &other) const

Так мы избавимся от ошибок вида

if (a + b = c + d)
    ...

Перегрузка арифметики

Обычно удобно сначала реализовывать операторы модификации: +=, -= и т. д., и лишь потом — бинарные операторы.

Это может показаться нелогичным, но давайте сравним два способа.

1. Сначала +, потом +=

Сначала определим оператор сложения. При этом возможно наткнуться на два случая утечки памяти:

const Example &operator + (const Example &other) const {
    Example result;
    ...
    return result;
}
    
/*
 Проблема в том, что мы возвращаем ссылке на `result`. Но так как `result` — временный
 объект, созданный на стеке, сразу после выхода из функции он удалится и полученная ссылка
 будет указывать на непонятно что.
    
 Физически, там еще будет записан старый объект `result`, но он перезапишется при следующем использовании стека.
 */
const Example &operator + (const Example &other) const {
    Example *result = new Example;
    ...
    return *result;
}

/*
 Теперь проблема в том, что `result` — переменная, выделенная в куче динамически,
 а значит ее нужно удалять вручную. Понятно, что в выражении `d = a + b + c` результат
 работы `a + b` никто не очистит.
 */

Правильным вариантом будет такой код:

const Example operator + (const Example &other) const {
    Example result;
    ...
    return result;
}

При этом произойдет сначала выделение памяти под result, потом — его копирование.

Теперь определим +=:

Example &operator += (const Example &other) {
  *this = *this + other;
  return *this;
}

Теперь происходит выделение памяти под result, потом — его копирование, а потом еще одно копирование (в C++11 для таких случаем изобрели move-семантику).

2. Сначала +=, потом +

Example &operator += (const Example &other) {
    ...
    return this;
}

Теперь можно легко реализовать бинарный оператор +:

const Example operator + (Example other) const {
    return other += *this;
}

Обратите внимание на то, что принимаемый тип не константен и принимается не по ссылке, а по значению.

Копирований всё еще два, но, что интересно, вторая оптимизируется, как правило, лучше.

Дружественные функции

Пусть у нашего класса определен конструктор от простого числа и оператор сложения:

Example (int a)
    ...

const Example &operator + (Example other) const {
    return other += *this;
}

Теперь, если мы хотим складывать с простым числом, можно сделать так b = a + 5. При этом компилятор увидит, что Example.operator+ принимает на вход Example, потом увидит конструктор Example(int a), после чего преобразует 5 в Example, и счастье настанет.

Но что, если написать b = 5 + a? Ничего хорошего: Example не приводится к int.

Тогда можно определить оператор сложения не как метод класса, а как функцию. При этом так как внутри этой функции нужен доступ к приватным переменным, она должна быть friend и обязательно реализована внутри класса (это для шаблонов).

friend const Example &operator + (Example l, const Example &r) {
    return l += r;
}

Кстати, заметьте, что теперь мы прибавляем к левому элементу правый, а наоборот, то есть если мы хотим деление, то

friend const Example &operator / (Example l, const Example &r) {
    return l /= r;
}

корректно, а

const Example &operator / (Example other) const {
    return other /= *this;
}

не корректно, так как делим не левый на правый, а правый на левый.

Так же можно реализовывать функции типа sqrt, pow и т. д.:

Example sqrt (const Example &r) {
    ...
}

В том числе, важные операторы

friend std::istream& operator >> (std::istream& in, const Example& a);
friend std::ostream& operator << (std::ostream& out, const Example& a);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment