Способы перегрузки операторов для класса

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

Иногда для удобства записи программ удобно определить арифметические операторы, например, операции сложения, вычитания, сравнения для пользовательских классов. Например, чтобы можно было писать:

В этом примере оператор «+» применяется для операндов типа date & и int. Можно «научить» компилятор реализовывать подобные операторы, это называется перегрузкой.

Возможность перегрузки есть только для пользовательских типов (классов). Перегружать можно только существующие в языке C++ операторы, нельзя «придумать» новые операторы.

Синтаксис перегрузки

Синтаксис перегрузки операторов очень похож на определение функции с именем operator@, где @ — это идентификатор оператора (например +, -, >, ==). Рассмотрим простейший пример:

class Integer
<
private:
int m_value;

public:
Integer(int i = 0) <
m_value = i;
>

int value() <
return m_value;
>

В этом примере объявлен класс Integer, который просто хранит одно число типа int. Оператор «+» для двух экземпляров класса integer можно объявить, как метод класса, принимающий правый операнд в виде параметра. То есть если в программе будет использована запись A + B, то она будет преобразована в вызов метода A.operator+(B). Левый операнд станет объектом, к которому будет вызван метод, правый операнд будет передан в качестве параметра. Мы передаем правый операнд, как константную ссылку на объект, сам метод также помечен, как константный метод, не модифицирующий объект. Метод возвращает объект по значению, которое создается при помощи явного вызова конструктора непосредственно в инструкции return.

Существует другой способ перегрузки операторов, как отдельных функций. В этом случае функция получает два параметра — левый и правый операнд. Пример перегрузки оператора при помощи такой функции:

В большинстве случаев, операторы (кроме условных) возвращают объект, или ссылку на тип, к которому относятся его аргументы (если типы разные, то вы сами решаете какого типа будет результат вычисления оператора).

Перегрузка унарных операторов

Рассмотрим примеры перегрузки унарных операторов для определенного выше класса Integer.

Перегрузим их как отдельные функции, а не как методы класса. Но при реализации операторов инкремента и декремента эта функция должна модифицировать закрытый член класса m_value, что казалось бы запрещено. Можно снять этот запрет для отдельных функций, которые объявляются, как «friend» — дружественные функции для нашего класса. Дружественные функции необходимо сначала описать внутри класса, предваряя описание словом «friend», затем реализовать их тело внутри класса.

class Integer
<
private:
int m_value;

public:
Integer(int i = 0) <
m_value = i;
>

int value() <
return m_value;
>

//префиксный инкремент
friend Integer& operator++(Integer&);

//постфиксный инкремент
friend Integer operator++(Integer&, int);

//префиксный декремент
friend Integer& operator—(Integer);

//постфиксный декремент
friend Integer operator—(Integer&, int);
>;

//префиксная версия возвращает значение после инкремента
Integer& operator++(Integer& i) <
i.m_value++;
return i;
>

//постфиксная версия возвращает значение до инкремента
Integer operator++(Integer& i, int) <
Integer oldValue = i;
i.m_value++;
return oldValue;
>

//префиксная версия возвращает значение после декремента
Integer& operator—(Integer& i) <
i.m_value—;
return i;
>

//постфиксная версия возвращает значение до декремента
const Integer operator—(Integer& i, int) <
Integer oldValue = i;
i.m_value—;
return oldValue;
>

Читайте также:  Gymnema sylvestre способ применения

Теперь вы знаете, как компилятор различает префиксные и постфиксные версии декремента и инкремента. В случае, когда он видит выражение ++i, то вызывается функция operator++(a). Если же он видит i++, то вызывается operator++(a, int). То есть вызывается перегруженная функция operator++, и именно для этого используется фиктивный параметр int в постфиксной версии.

Заметим, что оператор «+», а также префиксные инкременты, возвращают ссылку на сам объект, поэтому тип их возвращаемого значения — Integer&. Между тем операторы постфиксного инкремента и декремента и унарный «-» возвращают результат по значению, т.к. результат хранится во временной переменной, которая уничтожается после выхода из функции, поэтому эти операторы не могут возвращать результат по ссылке.

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

Рассмотрим синтаксис перегрузки бинарных операторов. Перегрузим один оператор, который возвращает значение l-value, один условный оператор и один оператор, создающий новое значение (определим их глобально):

class Integer
<
private:
int m_value;

public:
Integer(int i = 0) <
m_value = i;
>

int value() <
return m_value;
>

Во всех этих примерах операторы перегружаются для одного типа, однако, это необязательно. Можно, к примеру, перегрузить сложение нашего типа Integer и определенного по его подобию Float.

Аргументы и возвращаемые значения

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

Если аргумент не изменяется оператором, в случае, например унарного плюса, его нужно передавать как ссылку на константу. Это справедливо для почти всех арифметических операторов (сложение, вычитание, умножение. )

Тип возвращаемого значения зависит от сути оператора. Если оператор должен возвращать новое значение, то необходимо создавать новый объект (как в случае бинарного плюса) и возвращать его по значению. Если оператор возвращает ссылку на сам объект, как префиксные инкремент и декремент, то они должны возвращать ссылку на левый операнд.

Для операторов присваивания необходимо возвращать ссылку на измененный элемент. Также, если вы хотите использовать оператор присваивания в конструкциях вида (x=y).f(), где функция f() вызывается для переменной x, после присваивания ей y, то не возвращайте ссылку на константу, возвращайте просто ссылку.

Логические операторы должны возвращать в худшем случае int, а в лучшем bool.

Особые операторы

В C++ есть операторы, обладающие специфическим синтаксисом и способом перегрузки. Например оператор индексирования []. Он всегда определяется как член класса и, так как подразумевается поведение индексируемого объекта как массива, то ему следует возвращать ссылку.

Оператор запятая

В число «особых» операторов входит также оператор запятая. Он вызывается для объектов, рядом с которыми поставлена запятая (но он не вызывается в списках аргументов функций). Придумать осмысленный пример использования этого оператора не так-то просто и лучше его не перегружать.

Оператор разыменования указателя

Перегрузка этих операторов может быть оправдана для классов умных указателей. Этот оператор обязательно определяется как функция класса, причём на него накладываются некоторые ограничения: он должен возвращать либо объект (или ссылку), либо указатель, позволяющий обратиться к объекту.

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

Оператор присваивания обязательно определяется в виде функции класса, потому что он неразрывно связан с объектом, находящимся слева от «=». Определение оператора присваивания в глобальном виде сделало бы возможным переопределение стандартного поведения оператора «=». Пример:

Читайте также:  Способ получения энергии у растений

class Integer
<
private:
int m_value;

public:
Integer(int i = 0) <
m_value = i;
>

int value() <
return m_value;
>

Как можно заметить, в начале функции производится проверка на самоприсваивание. Вообще, в данном случае самоприсваивание безвредно, но ситуация не всегда такая простая. Например, если объект большой, можно потратить много времени на ненужное копирование, или при работе с указателями.

Неперегружаемые операторы

Некоторые операторы в C++ не перегружаются в принципе.

. оператор выбора члена класса.

.* оператор разыменования указателя на член класса

:: область видимости метода;

?: тернарный оператор сравнения;

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

Рекомендации к форме определения операторов

Как мы уже выяснили, существует два способа операторов — в виде функции класса и в виде дружественной глобальной функции.

Роб Мюррей, в своей книге C++ Strategies and Tactics определил следующие рекомендации по выбору формы оператора:

Источник

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

Доброго времени суток!

Желание написать данную статью появилось после прочтения поста Перегрузка C++ операторов, потому что в нём не были раскрыты многие важные темы.

Самое главное, что необходимо помнить — перегрузка операторов, это всего лишь более удобный способ вызова функций, поэтому не стоит увлекаться перегрузкой операторов. Использовать её следует только тогда, когда это упростит написание кода. Но, не настолько, чтобы это затрудняло чтение. Ведь, как известно, код читается намного чаще, чем пишется. И не забывайте, что вам никогда не дадут перегрузить операторы в тандеме со встроенными типами, возможность перегрузки есть только для пользовательских типов/классов.

Синтаксис перегрузки

Синтаксис перегрузки операторов очень похож на определение функции с именем operator@, где @ — это идентификатор оператора (например +, -, >). Рассмотрим простейший пример:

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

В большинстве случаев, операторы (кроме условных) возвращают объект, или ссылку на тип, к которому относятся его аргументы (если типы разные, то вы сами решаете как интерпретировать результат вычисления оператора).

Перегрузка унарных операторов

Рассмотрим примеры перегрузки унарных операторов для определенного выше класса Integer. Заодно определим их в виде дружественных функций и рассмотрим операторы декремента и инкремента:

Теперь вы знаете, как компилятор различает префиксные и постфиксные версии декремента и инкремента. В случае, когда он видит выражение ++i, то вызывается функция operator++(a). Если же он видит i++, то вызывается operator++(a, int). То есть вызывается перегруженная функция operator++, и именно для этого используется фиктивный параметр int в постфиксной версии.

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

Рассмотрим синтаксис перегрузки бинарных операторов. Перегрузим один оператор, который возвращает l-значение, один условный оператор и один оператор, создающий новое значение (определим их глобально):

Во всех этих примерах операторы перегружаются для одного типа, однако, это необязательно. Можно, к примеру, перегрузить сложение нашего типа Integer и определенного по его подобию Float.

Аргументы и возвращаемые значения

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

  • Если аргумент не изменяется оператором, в случае, например унарного плюса, его нужно передавать как ссылку на константу. Вообще, это справедливо для почти всех арифметических операторов (сложение, вычитание, умножение. )
  • Тип возвращаемого значения зависит от сути оператора. Если оператор должен возвращать новое значение, то необходимо создавать новый объект (как в случае бинарного плюса). Если вы хотите запретить изменение объекта как l-value, то нужно возвращать его константным.
  • Для операторов присваивания необходимо возвращать ссылку на измененный элемент. Также, если вы хотите использовать оператор присваивания в конструкциях вида (x=y).f(), где функция f() вызывается для для переменной x, после присваивания ей y, то не возвращайте ссылку на константу, возвращайте просто ссылку.
  • Логические операторы должны возвращать в худшем случае int, а в лучшем bool.
Читайте также:  Способ добывания пищи бабочки

Оптимизация возвращаемого значения

При создании новых объектов и возвращении их из функции следует использовать запись как для вышеописанного примера оператора бинарного плюса.

Честно говоря, не знаю, какая ситуация актуальна для C++11, все рассуждения далее справедливы для C++98.
На первый взгляд, это похоже на синтаксис создания временного объекта, то есть как будто бы нет разницы между кодом выше и этим:

Но на самом деле, в этом случае произойдет вызов конструктора в первой строке, далее вызов конструктора копирования, который скопирует объект, а далее, при раскрутке стека вызовется деструктор. При использовании первой записи компилятор изначально создаёт объект в памяти, в которую нужно его скопировать, таким образом экономится вызов конструктора копирования и деструктора.

Особые операторы

В C++ есть операторы, обладающие специфическим синтаксисом и способом перегрузки. Например оператор индексирования []. Он всегда определяется как член класса и, так как подразумевается поведение индексируемого объекта как массива, то ему следует возвращать ссылку.

Оператор запятая

В число «особых» операторов входит также оператор запятая. Он вызывается для объектов, рядом с которыми поставлена запятая (но он не вызывается в списках аргументов функций). Придумать осмысленный пример использования этого оператора не так-то просто. Хабраюзер AxisPod в комментариях к предыдущей статье о перегрузке рассказал об одном.

Оператор разыменования указателя

Перегрузка этих операторов может быть оправдана для классов умных указателей. Этот оператор обязательно определяется как функция класса, причём на него накладываются некоторые ограничения: он должен возвращать либо объект (или ссылку), либо указатель, позволяющий обратиться к объекту.

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

Оператор присваивания обязательно определяется в виде функции класса, потому что он неразрывно связан с объектом, находящимся слева от «=». Определение оператора присваивания в глобальном виде сделало бы возможным переопределение стандартного поведения оператора «=». Пример:

Как можно заметить, в начале функции производится проверка на самоприсваивание. Вообще, в данном случае самоприсваивание безвредно, но ситуация не всегда такая простая. Например, если объект большой, можно потратить много времени на ненужное копирование, или при работе с указателями.

Неперегружаемые операторы

Некоторые операторы в C++ не перегружаются в принципе. По всей видимости, это сделано из соображений безопасности.

  • Оператор выбора члена класса «.».
  • Оператор разыменования указателя на член класса «.*»
  • В С++ отсутствует оператор возведения в степень (как в Fortran) «**».
  • Запрещено определять свои операторы (возможны проблемы с определением приоритетов).
  • Нельзя изменять приоритеты операторов

Источник

Оцените статью
Разные способы