Основы перегрузки операторов. Основы перегрузки операторов Что значит переопределение в c

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

Char str1 = "Hello "; char str2 = "world!"; str1 + str2;

и в результате получим строку «Hello world!». Правда, было бы замечательно? Ну так пожалуйста! Сегодня вы научитесь «объяснять» компьютеру, что оператором + вы хотите сложить не два числа, а две строки. И работа со строками — это один из самых удачных, на мой взгляд, примеров, чтобы начать разбираться с темой «Перегрузка операторов».

Приступим к практике. В этом примере мы перегрузим оператор + и заставим его к одной строке дописывать содержимое другой строки. А именно: мы соберем из четырех отдельных строк часть известного всем нам стиха А.С.Пушкина. Советую открыть вашу среду разработки и переписать этот пример. Если вам не все будет понятно в коде, не волнуйтесь, ниже будут приведены подробные объяснения.

#include #include using namespace std; class StringsWork { private: char str;//строка, которая доступна классу public: StringsWork()//конструктор в котором очистим строку класса от мусора { for(int i = 0; i < 256; i++) str[i] = "\0"; } void operator +(char*);//прототип метода класса в котором мы перегрузим оператор + void getStr();//метод вывода данных на экран }; void StringsWork::operator +(char *s) //что должен выполнить оператор + { strcat(str, s); //сложение строк } void StringsWork::getStr() { cout << str << endl << endl;//вывод символьного массива класса на экран } int main() { setlocale(LC_ALL, "rus"); char *str1 = new char ; //выделим память для строк char *str2 = new char ; char *str3 = new char ; char *str4 = new char ; strcpy(str1,"У лукоморья дуб зелёный;\n");//инициализируем strcpy(str2,"Всё ходит по цепи кругом;\n"); strcpy(str3,"И днём и ночью кот учёный\n"); strcpy(str4,"Златая цепь на дубе том:\n"); cout << "1) " << str1; cout << "2) " << str2; cout << "3) " << str3; cout << "4) " << str4 << endl; StringsWork story;//создаем объект и добавяем в него строки с помощью перегруженного + story + str1; story + str4; story + str3; story + str2; cout << "========================================" << endl; cout << "Стих, после правильного сложения строк: " << endl; cout << "========================================" << endl << endl; story.getStr(); //Отмечу, что для числовых типов данных оператор плюс будет складывать значения, как и должен int a = 5; int b = 5; int c = 0; c = a + b; cout << "========================================" << endl << endl; cout << "a = " << a << endl << "b = " << b << endl; cout << "c = " << a << " + " << b << " = " << c << endl << endl; delete str4;//освободим память delete str3; delete str2; delete str1; return 0; }

Разберемся:

Что-то новое в коде мы увидели в строке 16 void operator +(char*); Тут мы объявили прототип метода класса в котором перегрузим наш оператор + . Чтобы перегрузить оператор необходимо использовать зарезервированное слово operator . Выглядит это так, словно вы определяете обычную функцию: void operator+ () {//код} В теле этой функции мы размещаем код, который покажет компилятору, какие действия будет выполнять оператор + (или какой-либо другой оператор). Перегруженный оператор будет выполнять указанные для него действия только в пределах того класса, в котором он определен. Ниже, в строках 20 — 23 мы уже определяем какую роль будет играть + в нашем классе. А именно, с помощью функции strcat (str, s); он будет дописывать содержимое строки s , которую мы передали по указателю, в конец строки str . Строки 17, 25 — 28 это обычный метод класса, с помощью которого строка класса будет показана на экран. Если вам не понятно, как определять методы класса вне тела класса, т.е. такой момент как void StringsWork::getStr() {//определение} , то вам сначала желательно сходить сюда . Далее, уже в главной функции main() , в строках 34 — 37 ,создаем четыре указателя на строки и выделяем необходимое количество памяти для каждой из них, не забывая о том, что для символа "\0" так же надо зарезервировать одну ячейку char *str1 = new char ; . Затем копируем в них текст с помощью функции strcpy() и показываем их на экран — строки 39 — 47 . А в строке 49 создаем объект класса. При его создании сработает конструктор класса и строка класса будет очищена от лишних данных. Теперь нам остается только сложить строки в правильной последовательности, используя перегруженный оператор + — строки 50 — 53 и посмотреть, что получилось — строка 58 .

Результат работы программы:

1) У лукоморья дуб зелёный;
2) Всё ходит по цепи кругом;
3) И днём и ночью кот учёный
4) Златая цепь на дубе том:

========================================
Стих, после правильного сложения строк:

У лукоморья дуб зелёный;
Златая цепь на дубе том:
И днём и ночью кот учёный
Всё ходит по цепи кругом;
========================================

a = 5
b = 5
c = 5 + 5 = 10

Ограничения перегрузки операторов

  • перегрузить можно практически любой оператор, за исключением следующих:

. точка (выбор элемента класса);

* звездочка (определение или разыменование указателя);

:: двойное двоеточие (область видимости метода);

?: знак вопроса с двоеточием (тернарный оператор сравнения);

# диез (символ препроцессора);

## двойной диез (символ препроцессора);

sizeof оператор нахождения размера объекта в байтах;

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

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

Нельзя не отметить, что многие программисты негативно относятся к перегрузке операторов. Сама возможность перегрузки операторов предоставлена для облегчения понимания и читаемости кода программ. В то же время, она наоборот может стать и причиной усложнения вашей программы и многим программистам будет тяжело ее понять. Помните о «золотой середине» и используйте перегрузку только тогда, когда она реально принесет пользу вам и другим. Вполне можно обойтись и без перегрузки операторов. Но это не значит, что можно проигнорировать данную тему. В ней следует разобраться хотя бы потому, что вам когда-то придется столкнуться с перегрузкой в чужом коде и вы сможете легко разобраться что к чему.

Вот мы очень коротко ознакомились с перегрузкой операторов в С++. Увидели, так сказать, вершину айсберга. А вашим домашним заданием (ДА-ДА — ДОМАШНИМ ЗАДАНИЕМ!) будет доработать программу, добавив в нее перегрузку оператора для удаления строки. Какой оператор перегружать выберите сами. Либо предложите свой вариант апгрейда кода, добавив в него то, что посчитаете нужным и интересным. Ваши «труды» можете добавлять в комментарии к этой статье. Нам интересно будет посмотреть ваши варианты решения. Удачи!

Основы перегрузки операторов

В C#, подобно любому языку программирования, имеется готовый набор лексем, используемых для выполнения базовых операций над встроенными типами. Например, известно, что операция + может применяться к двум целым, чтобы дать их сумму:

// Операция + с целыми. int а = 100; int b = 240; int с = а + b; //с теперь равно 340

Здесь нет ничего нового, но задумывались ли вы когда-нибудь о том, что одна и та же операция + может применяться к большинству встроенных типов данных C#? Например, рассмотрим такой код:

// Операция + со строками. string si = "Hello"; string s2 = " world!"; string s3 = si + s2; // s3 теперь содержит "Hello world!"

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

Язык C# предоставляет возможность строить специальные классы и структуры, которые также уникально реагируют на один и тот же набор базовых лексем (вроде операции +). Имейте в виду, что абсолютно каждую встроенную операцию C# перегружать нельзя. В следующей таблице описаны возможности перегрузки основных операций:

Операция C# Возможность перегрузки
+, -, !, ++, --, true, false Этот набор унарных операций может быть перегружен
+, -, *, /, %, &, |, ^, > Эти бинарные операции могут быть перегружены
==, !=, <, >, <=, >= Эти операции сравнения могут быть перегружены. C# требует совместной перегрузки "подобных" операций (т.е. < и >, <= и >=, == и!=)
Операция не может быть перегружена. Oднако, аналогичную функциональность предлагают индексаторы
() Операция () не может быть перегружена. Однако ту же функциональность предоставляют специальные методы преобразования
+=, -=, *=, /=, %=, &=, |=, ^=, >= Сокращенные операции присваивания не могут перегружаться; однако вы получаете их автоматически, перегружая соответствующую бинарную операцию

Перегрузка операторов тесно связана с перегрузкой методов. Для перегрузки оператора служит ключевое слово operator , определяющее операторный метод, который, в свою очередь, определяет действие оператора относительно своего класса. Существуют две формы операторных методов (operator): одна - для унарных операторов, другая - для бинарных. Ниже приведена общая форма для каждой разновидности этих методов:

// Общая форма перегрузки унарного оператора. public static возвращаемый_тип operator op(тип_параметра операнд) { // операции } // Общая форма перегрузки бинарного оператора. public static возвращаемый_тип operator op(тип_параметра1 операнд1, тип_параметра2 операнд2) { // операции }

Здесь вместо op подставляется перегружаемый оператор, например + или /, а возвращаемый_тип обозначает конкретный тип значения, возвращаемого указанной операцией. Это значение может быть любого типа, но зачастую оно указывается такого же типа, как и у класса, для которого перегружается оператор. Такая корреляция упрощает применение перегружаемых операторов в выражениях. Для унарных операторов операнд обозначает передаваемый операнд, а для бинарных операторов то же самое обозначают операнд1 и операнд2 . Обратите внимание на то, что операторные методы должны иметь оба спецификатора типа - public и static.

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

Давайте рассмотрим применение перегрузки бинарных операторов на простейшем примере:

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class MyArr { // Координаты точки в трехмерном пространстве public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) { this.x = x; this.y = y; this.z = z; } // Перегружаем бинарный оператор + public static MyArr operator +(MyArr obj1, MyArr obj2) { MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr.y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; } // Перегружаем бинарный оператор - public static MyArr operator -(MyArr obj1, MyArr obj2) { MyArr arr = new MyArr(); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; return arr; } } class Program { static void Main(string args) { MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Координаты первой точки: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine("Координаты второй точки: " + Point2.x + " " + Point2.y + " " + Point2.z + "\n"); MyArr Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2; Console.WriteLine("\nPoint1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Console.ReadLine(); } } }

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

Унарные операторы перегружаются таким же образом, как и бинарные. Главное отличие заключается, конечно, в том, что у них имеется лишь один операнд. Давайте модернизируем предыдущий пример, дополнив перегрузки операций ++, --, -:

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class MyArr { // Координаты точки в трехмерном пространстве public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) { this.x = x; this.y = y; this.z = z; } // Перегружаем бинарный оператор + public static MyArr operator +(MyArr obj1, MyArr obj2) { MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr.y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; } // Перегружаем бинарный оператор - public static MyArr operator -(MyArr obj1, MyArr obj2) { MyArr arr = new MyArr(); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; return arr; } // Перегружаем унарный оператор - public static MyArr operator -(MyArr obj1) { MyArr arr = new MyArr(); arr.x = -obj1.x; arr.y = -obj1.y; arr.z = -obj1.z; return arr; } // Перегружаем унарный оператор ++ public static MyArr operator ++(MyArr obj1) { obj1.x += 1; obj1.y += 1; obj1.z +=1; return obj1; } // Перегружаем унарный оператор -- public static MyArr operator --(MyArr obj1) { obj1.x -= 1; obj1.y -= 1; obj1.z -= 1; return obj1; } } class Program { static void Main(string args) { MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Координаты первой точки: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine("Координаты второй точки: " + Point2.x + " " + Point2.y + " " + Point2.z + "\n"); MyArr Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2; Console.WriteLine("Point1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = -Point1; Console.WriteLine("-Point1 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point2++; Console.WriteLine("Point2++ = " + Point2.x + " " + Point2.y + " " + Point2.z); Point2--; Console.WriteLine("Point2-- = " + Point2.x + " " + Point2.y + " " + Point2.z); Console.ReadLine(); } } }

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

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

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

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

Синтаксис перегрузки операторов очень похож на определение функции с именем operator@, где @ - это идентификатор оператора (например +, -, <<, >>). Рассмотрим простейший пример:
class Integer { private: int value; public: Integer(int i): value(i) {} const Integer operator+(const Integer& rv) const { return (value + rv.value); } };
В данном случае, оператор оформлен как член класса, аргумент определяет значение, находящееся в правой части оператора. Вообще, существует два основных способа перегрузки операторов: глобальные функции, дружественные для класса, или подставляемые функции самого класса. Какой способ, для какого оператора лучше, рассмотрим в конце топика.

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

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

Рассмотрим примеры перегрузки унарных операторов для определенного выше класса Integer. Заодно определим их в виде дружественных функций и рассмотрим операторы декремента и инкремента:
class Integer { private: int value; public: Integer(int i): value(i) {} //унарный + friend const Integer& operator+(const Integer& i); //унарный - friend const Integer operator-(const Integer& i); //префиксный инкремент friend const Integer& operator++(Integer& i); //постфиксный инкремент friend const Integer operator++(Integer& i, int); //префиксный декремент friend const Integer& operator--(Integer& i); //постфиксный декремент friend const Integer operator--(Integer& i, int); }; //унарный плюс ничего не делает. const Integer& operator+(const Integer& i) { return i.value; } const Integer operator-(const Integer& i) { return Integer(-i.value); } //префиксная версия возвращает значение после инкремента const Integer& operator++(Integer& i) { i.value++; return i; } //постфиксная версия возвращает значение до инкремента const Integer operator++(Integer& i, int) { Integer oldValue(i.value); i.value++; return oldValue; } //префиксная версия возвращает значение после декремента const Integer& operator--(Integer& i) { i.value--; return i; } //постфиксная версия возвращает значение до декремента const Integer operator--(Integer& i, int) { Integer oldValue(i.value); i.value--; return oldValue; }
Теперь вы знаете, как компилятор различает префиксные и постфиксные версии декремента и инкремента. В случае, когда он видит выражение ++i, то вызывается функция operator++(a). Если же он видит i++, то вызывается operator++(a, int). То есть вызывается перегруженная функция operator++, и именно для этого используется фиктивный параметр int в постфиксной версии.

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

Рассмотрим синтаксис перегрузки бинарных операторов. Перегрузим один оператор, который возвращает l-значение, один условный оператор и один оператор, создающий новое значение (определим их глобально):
class Integer { private: int value; public: Integer(int i): value(i) {} friend const Integer operator+(const Integer& left, const Integer& right); friend Integer& operator+=(Integer& left, const Integer& right); friend bool operator==(const Integer& left, const Integer& right); }; const Integer operator+(const Integer& left, const Integer& right) { return Integer(left.value + right.value); } Integer& operator+=(Integer& left, const Integer& right) { left.value += right.value; return left; } bool operator==(const Integer& left, const Integer& right) { return left.value == right.value; }
Во всех этих примерах операторы перегружаются для одного типа, однако, это необязательно. Можно, к примеру, перегрузить сложение нашего типа Integer и определенного по его подобию Float.

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

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

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

При создании новых объектов и возвращении их из функции следует использовать запись как для вышеописанного примера оператора бинарного плюса.
return Integer(left.value + right.value);
Честно говоря, не знаю, какая ситуация актуальна для C++11, все рассуждения далее справедливы для C++98.
На первый взгляд, это похоже на синтаксис создания временного объекта, то есть как будто бы нет разницы между кодом выше и этим:
Integer temp(left.value + right.value); return temp;
Но на самом деле, в этом случае произойдет вызов конструктора в первой строке, далее вызов конструктора копирования, который скопирует объект, а далее, при раскрутке стека вызовется деструктор. При использовании первой записи компилятор изначально создаёт объект в памяти, в которую нужно его скопировать, таким образом экономится вызов конструктора копирования и деструктора.

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

В C++ есть операторы, обладающие специфическим синтаксисом и способом перегрузки. Например оператор индексирования . Он всегда определяется как член класса и, так как подразумевается поведение индексируемого объекта как массива, то ему следует возвращать ссылку.
Оператор запятая
В число «особых» операторов входит также оператор запятая. Он вызывается для объектов, рядом с которыми поставлена запятая (но он не вызывается в списках аргументов функций). Придумать осмысленный пример использования этого оператора не так-то просто. Хабраюзер AxisPod в комментариях к предыдущей статье о перегрузке рассказал об одном .
Оператор разыменования указателя
Перегрузка этих операторов может быть оправдана для классов умных указателей. Этот оператор обязательно определяется как функция класса, причём на него накладываются некоторые ограничения: он должен возвращать либо объект (или ссылку), либо указатель, позволяющий обратиться к объекту.
Оператор присваивания
Оператор присваивания обязательно определяется в виде функции класса, потому что он неразрывно связан с объектом, находящимся слева от "=". Определение оператора присваивания в глобальном виде сделало бы возможным переопределение стандартного поведения оператора "=". Пример:
class Integer { private: int value; public: Integer(int i): value(i) {} Integer& operator=(const Integer& right) { //проверка на самоприсваивание if (this == &right) { return *this; } value = right.value; return *this; } };

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

Неперегружаемые операторы
Некоторые операторы в C++ не перегружаются в принципе. По всей видимости, это сделано из соображений безопасности.
  • Оператор выбора члена класса ".".
  • Оператор разыменования указателя на член класса ".*"
  • В С++ отсутствует оператор возведения в степень (как в Fortran) "**".
  • Запрещено определять свои операторы (возможны проблемы с определением приоритетов).
  • Нельзя изменять приоритеты операторов
Как мы уже выяснили, существует два способа операторов - в виде функции класса и в виде дружественной глобальной функции.
Роб Мюррей, в своей книге C++ Strategies and Tactics определил следующие рекомендации по выбору формы оператора:

Почему так? Во-первых, на некоторые операторы изначально наложено ограничение. Вообще, если семантически нет разницы как определять оператор, то лучше его оформить в виде функции класса, чтобы подчеркнуть связь, плюс помимо этого функция будет подставляемой (inline). К тому же, иногда может возникнуть потребность в том, чтобы представить левосторонний операнд объектом другого класса. Наверное, самый яркий пример - переопределение << и >> для потоков ввода/вывода.

Последнее обновление: 20.10.2017

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

Перегрузить можно только те операторы, которые уже определены в C++. Создать новые операторы нельзя.

Если функция оператора определена как отдельная функция и не является членом класса, то количество параметров такой функции совпадает с количеством операндов оператора. Например, у функции, которая представляет унарный оператор, будет один параметр, а у функции, которая представляет бинарный оператор, - два параметра. Если оператор принимает два операнда, то первый операнд передается первому параметру функции, а второй операнд - второму параметру. При этом как минимум один из параметров должен представлять тип класса

Рассмотрим пример с классом Counter, который представляет секундомер и хранит количество секунд:

#include << seconds << " seconds" << std::endl; } int seconds; }; Counter operator + (Counter c1, Counter c2) { return Counter(c1.seconds + c2.seconds); } int main() { Counter c1(20); Counter c2(10); Counter c3 = c1 + c2; c3.display(); // 30 seconds return 0; }

Здесь функция оператора не является частью класса Counter и определена вне его. Данная функция перегружает оператор сложения для типа Counter. Она является бинарной, поэтому принимает два параметра. В данном случае мы складываем два объекта Counter. Возвращает функция также объект Counter, который хранит общее количесто секунд. То есть по сути здесь операция сложения сводится к сложению секунд обоих объектов:

Counter operator + (Counter c1, Counter c2) { return Counter(c1.seconds + c2.seconds); }

При этом необязательно возвращать объект класса. Это может быть и объект встроенного примитивного типа. И также мы можем определять дополнительные перегруженные функции операторов:

Int operator + (Counter c1, int s) { return c1.seconds + s; }

Данная версия складывает объект Counter с числом и возвращает также число. Поэтому левый операнд операции должен представлять тип Counter, а правый операнд - тип int. И, к примеру, мы можем применить данную версию оператора следующим образом:

Counter c1(20); int seconds = c1 + 25; // 45 std::cout << seconds << std::endl;

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

#include class Counter { public: Counter(int sec) { seconds = sec; } void display() { std::cout << seconds << " seconds" << std::endl; } Counter operator + (Counter c2) { return Counter(this->seconds + c2.seconds); } int operator + (int s) { return this->seconds + s; } int seconds; }; int main() { Counter c1(20); Counter c2(10); Counter c3 = c1 + c2; c3.display(); // 30 seconds int seconds = c1 + 25; // 45 return 0; }

В данном случае к левому операнду в функциях операторов мы обращаемся через указатель this.

Какие операторы где переопределять? Операторы присвоения, индексирования (), вызова (()), доступа к члену класса по указателю (->) следует определять в виде функций-членов класса. Операторы, которые изменяют состояние объекта или непосредственно связаны с объектом (инкремент, декремент,), обычно также определяются в виде функций-членов класса. Все остальные операторы чаще определяются как отдельные функции, а не члены класса.

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

Ряд операторов перегружаются парами. Например, если мы определяем оператор == , то необходимо также определить и оператор != . А при определении оператора < надо также определять функцию для оператора > . Например, перегрузим данные операторы:

Bool operator == (Counter c1, Counter c2) { return c1.seconds == c2.seconds; } bool operator != (Counter c1, Counter c2) { return c1.seconds != c2.seconds; } bool operator > (Counter c1, Counter c2) { return c1.seconds > c2.seconds; } bool operator < (Counter c1, Counter c2) { return c1.seconds < c2.seconds; } int main() { Counter c1(20); Counter c2(10); bool b1 = c1 == c2; // false bool b2 = c1 > c2; // true std::cout << b1 << std::endl; std::cout << b2 << std::endl; return 0; }

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

#include class Counter { public: Counter(int sec) { seconds = sec; } void display() { std::cout << seconds << " seconds" << std::endl; } Counter& operator += (Counter c2) { seconds += c2.seconds; return *this; } int seconds; }; int main() { Counter c1(20); Counter c2(10); c1 += c2; c1.display(); // 30 seconds return 0; }

Операции инкремента и декремента

Особую сложность может представлять переопределение операций инкремента и декремента, поскольку нам надо определить и префиксную, и постфиксную форму для этих операторов. Определим подобные операторы для типа Counter:

#include class Counter { public: Counter(int sec) { seconds = sec; } void display() { std::cout << seconds << " seconds" << std::endl; } // префиксные операторы Counter& operator++ () { seconds += 5; return *this; } Counter& operator-- () { seconds -= 5; return *this; } // постфиксные операторы Counter operator++ (int) { Counter prev = *this; ++*this; return prev; } Counter operator-- (int) { Counter prev = *this; --*this; return prev; } int seconds; }; int main() { Counter c1(20); Counter c2 = c1++; c2.display(); // 20 seconds c1.display(); // 25 seconds --c1; c1.display(); // 20 seconds return 0; }

Counter& operator++ () { seconds += 5; return *this; }

В самой функции можно определить некоторую логику по инкременту значения. В данном случае количество секунд увеличивается на 5.

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

Counter operator++ (int) { Counter prev = *this; ++*this; return prev; }

Перегрузка операторов в C++. Способы применения

В мы рассмотрели основные аспекты использования перегрузки операторов. В этом материалы вашему вниманию будут представлены перегружаемые операторы C++. Для каждого раздела характерна семантика, т.е. ожидаемое поведение. Кроме того, будут показаны типичные способы объявления и реализации операторов.

В примерах кода X означает пользовательский тип, для которого реализован оператор. T - это необязательный тип, пользовательский либо встроенный. Параметры бинарного оператора будут называться lhs и rhs . Если оператор будет объявлен как метод класса, у его объявления будет префикс X:: .

operator=

  • Определение справа налево : в отличие от большинства операторов, operator= правоассоциативен, т.е. a = b = c означает a = (b = c) .

Копирование

  • Семантика : присваивание a = b . Значение или состояние b передаётся a . Кроме того, возвращается ссылка на a . Это позволяет создавать цепочки вида c = a = b .
  • Типичное объявление : X& X::operator= (X const& rhs) . Возможны другие типы аргументов, но используется это нечасто.
  • Типичная реализация : X& X::operator= (X const& rhs) { if (this != &rhs) { //perform element wise copy, or: X tmp(rhs); //copy constructor swap(tmp); } return *this; }

Перемещение (начиная с C++11)

  • Семантика : присваивание a = temporary() . Значение или состояние правой величины присваивается a путём перемещения содержимого. Возвращается ссылка на a .
  • : X& X::operator= (X&& rhs) { //take the guts from rhs return *this; }
  • Сгенерированный компилятором operator= : компилятор может создать только два вида этого оператора. Если же оператор не объявлен в классе, компилятор пытается создать публичные операторы копирования и перемещения. Начиная с C++11 компилятор может создавать оператор по умолчанию: X& X::operator= (X const& rhs) = default;

    Сгенерированный оператор просто копирует/перемещает указанный элемент, если такая операция разрешена.

operator+, -, *, /, %

  • Семантика : операции сложения, вычитания, умножения, деления, деления с остатком. Возвращается новый объект с результирующим значением.
  • Типичные объявление и реализация : X operator+ (X const lhs, X const rhs) { X tmp(lhs); tmp += rhs; return tmp; }

    Обычно, если существует operator+ , имеет смысл также перегрузить и operator+= для того, чтобы использовать запись a += b вместо a = a + b . Если же operator+= не перегружен, реализация будет выглядеть примерно так:

    X operator+ (X const& lhs, X const& rhs) { // create a new object that represents the sum of lhs and rhs: return lhs.plus(rhs); }

Унарные operator+, –

  • Семантика : положительный или отрицательный знак. operator+ обычно ничего не делает и поэтому почти не используется. operator- возвращает аргумент с противоположным знаком.
  • Типичные объявление и реализация : X X::operator- () const { return /* a negative copy of *this */; } X X::operator+ () const { return *this; }

operator<<, >>

  • Семантика : во встроенных типах операторы используются для битового сдвига левого аргумента. Перегрузка этих операторов с именно такой семантикой встречается редко, на ум приходит лишь std::bitset . Однако, для работы с потоками была введена новая семантика, и перегрузка операторов ввода/вывода весьма распространена.
  • Типичные объявление и реализация : поскольку в стандартные классы iostream добавлять методы нельзя, операторы сдвига для определённых вами классов нужно перегружать в виде свободных функций: ostream& operator<< (ostream& os, X const& x) { os << /* the formatted data of rhs you want to print */; return os; } istream& operator>> (istream& is, X& x) { SomeData sd; SomeMoreData smd; if (is >> sd >> smd) { rhs.setSomeData(sd); rhs.setSomeMoreData(smd); } return lhs; }

    Кроме того, тип левого операнда может быть любым классом, которые должен вести себя как объект ввода/вывода, то есть правый операнд может быть и встроенного типа.

    MyIO& MyIO::operator<< (int rhs) { doYourThingWith(rhs); return *this; }

Бинарные operator&, |, ^

  • Семантика : Битовые операции “и”, “или”, “исключающее или”. Эти операторы перегружаются очень редко. Опять же, единственным примером является std::bitset .

operator+=, -=, *=, /=, %=

  • Семантика : a += b обычно означает то же, что и a = a + b . Поведение остальных операторов аналогично.
  • Типичные определение и реализация : поскольку операция изменяет левый операнд, скрытое приведение типов нежелательно. Поэтому эти операторы должны быть перегружены как методы класса. X& X::operator+= (X const& rhs) { //apply changes to *this return *this; }

operator&=, |=, ^=, <<=, >>=

  • Семантика : аналогична operator+= , но для логических операций. Эти операторы перегружаются так же редко, как и operator| и т.д. operator<<= и operator>>= не используются для операций ввода/вывода, поскольку operator<< и operator>> уже изменяют левый аргумент.

operator==, !=

  • Семантика : проверка на равенство/неравенство. Смысл равенства очень сильно зависит от класса. В любом случае, учитывайте следующие свойства равенств:
    1. Рефлексивность, т.е. a == a .
    2. Симметричность, т.е. если a == b , то b == a .
    3. Транзитивность, т.е. если a == b и b == c , то a == c .
  • Типичные объявление и реализация : bool operator== (X const& lhs, X cosnt& rhs) { return /* check for whatever means equality */ } bool operator!= (X const& lhs, X const& rhs) { return !(lhs == rhs); }

    Вторая реализация operator!= позволяет избежать повторов кода и исключает любую возможную неопределённость в отношении любых двух объектов.

operator<, <=, >, >=

  • Семантика : проверка на соотношение (больше, меньше и т.д.). Обычно используется, если порядок элементов однозначно определён, то есть сложные объекты с несколькими характеристиками сравнивать бессмысленно.
  • Типичные объявление и реализация : bool operator< (X const& lhs, X const& rhs) { return /* compare whatever defines the order */ } bool operator> (X const& lhs, X const& rhs) { return rhs < lhs; }

    Реализация operator> с использованием operator< или наоборот обеспечивает однозначное определение. operator<= может быть реализован по-разному, в зависимости от ситуации . В частности, при отношении строго порядка operator== можно реализовать лишь через operator< :

    Bool operator== (X const& lhs, X const& rhs) { return !(lhs < rhs) && !(rhs < lhs); }

operator++, –

  • Семантика : a++ (постинкремент) увеличивает значение на 1 и возвращает старое значение. ++a (преинкремент) возвращает новое значение. С декрементом operator-- все аналогично.
  • Типичные объявление и реализация : X& X::operator++() { //preincrement /* somehow increment, e.g. *this += 1*/; return *this; } X X::operator++(int) { //postincrement X oldValue(*this); ++(*this); return oldValue; }

operator()

  • Семантика : исполнение объекта-функции (функтора). Обычно используется не для изменения объекта, а для использования его в качестве функции.
  • Нет ограничений на параметры : в отличие от прошлых операторов, в этом случае нет никаких ограничений на количество и тип параметров. Оператор может быть перегружен только как метод класса.
  • Пример объявления : Foo X::operator() (Bar br, Baz const& bz);

operator

  • Семантика : доступ к элементам массива или контейнера, например, в std::vector , std::map , std::array .
  • Объявление : тип параметра может быть любым. Тип возвращаемого значения обычно является ссылкой на то, что хранится в контейнере. Часто оператор перегружается в двух версиях, константной и неконстантной: Element_t& X::operator(Index_t const& index); const Element_t& X::operator(Index_t const& index) const;

operator!

  • Семантика : отрицание в логическом смысле.
  • Типичные объявление и реализация : bool X::operator!() const { return !/*some evaluation of *this*/; }

explicit operator bool

  • Семантика : использования в логическом контексте. Чаще всего используется с умными указателями.
  • Реализация : explicit X::operator bool() const { return /* if this is true or false */; }

operator&&, ||

  • Семантика : логические “и”, “или”. Эти операторы определены только для встроенного логического типа и работают по “ленивому” принципу, то есть второй аргумент рассматривается, только если первый не определяет результат. При перегрузке это свойство теряется, поэтому перегружают эти операторы редко.

Унарный operator*

  • Семантика : разыменовывание указателя. Обычно перегружается для классов с умными указателями и итераторами. Возвращает ссылку на то, куда указывает объект.
  • Типичные объявление и реализация : T& X::operator*() const { return *_ptr; }

operator->

  • Семантика : доступ к полю по указателю. Как и предыдущий, этот оператор перегружается для использования с умными указателями и итераторами. Если в коде встречается оператор -> , компилятор перенаправляет вызовы на operator-> , если возвращается результат пользовательского типа.
  • Usual implementation : T* X::operator->() const { return _ptr; }

operator->*

  • Семантика : доступ к указателю-на-поле по указателю. Оператор берёт указатель на поле и применяет его к тому, на что указывает *this , то есть objPtr->*memPtr - это то же самое, что и (*objPtr).*memPtr . Используется очень редко.
  • Возможная реализация : template T& X::operator->*(T V::* memptr) { return (operator*()).*memptr; }

    Здесь X - это умный указатель, V - тип, на который указывает X , а T - тип, на который указывает указатель-на-поле. Неудивительно, что этот оператор редко перегружают.

Унарный operator&

  • Семантика : адресный оператор. Этот оператор перегружают очень редко.

operator,

  • Семантика : встроенный оператор “запятая”, применённый к двум выражениям, выполняет их оба в порядке записи и возвращает значение второго из них. Перегружать его не рекомендуется.

operator~

  • Семантика : оператор побитовой инверсии. Один из наиболее редко используемых операторов.

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

  • Семантика : позволяет скрытое или явное приведение объектов класса к другим типам.
  • Объявление : //conversion to T, explicit or implicit X::operator T() const; //explicit conversion to U const& explicit X::operator U const&() const; //conversion to V& V& X::operator V&();

    Эти объявления выглядят странно, поскольку в них отсутствует тип возвращаемого значения. Он является частью имени оператора у не указывается дважды. Стоит помнить, что большое количество скрытых приведений может повлечь за собой непредвиденные ошибки в работе программы.

operator new, new, delete, delete

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

Заключение

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