Доброго времени суток, уважаемые читатели Хабра!

Когда я только начал свой путь по изучению C++, у меня возникало много вопросов, на которые, порой, не удавалось быстро найти ответов. Не стала исключением и такая тема как перегрузка операторов. Теперь, когда я разобрался в этой теме, я хочу помочь другим расставить все точки над i.

В этой публикации я расскажу: о различных тонкостях перегрузки операторов, зачем вообще нужна эта перегрузка, о типах операторов (унарные/бинарные), о перегрузке оператора с friend (дружественная функция), а так же о типах принимаемых и возвращаемых перегрузками значений.

UPD.: Для тех, кто предпочитает видео-формат есть видео: https://youtu.be/Qn6mu9l6Xj8

Для чего нужна перегрузка?


Предположим, что вы создаете свой класс или структуру, пусть он будет описывать вектор в 3-х мерном пространстве:

struct Vector3
{
	int x, y, z;

	Vector3()
	{}
	Vector3(int x, int y, int z) : x(x), y(y), z(z)
	{}
};

Теперь, Вы создаете 3 объекта этой структуры:

Vector3 v1(10, 10, 10), v2(20, 20, 25), v3;
//...

И хотите прировнять объект v2 объекту v1, пишете:

v1 = v2;

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

Поэтому нам необходимо перегрузить оператор присваивания (=).

Общие сведения о перегрузке операторов


Для этого добавим в нашу структуру перегрузку:

Vector3 operator = (Vector3 v1)
{
	return Vector3(this->x = v1.x, this->y = v1.y, this->z = 0);
}

Теперь, в коде выше мы указали, что при присваивании необходимо скопировать переменные x и y, а z обнулить.

UPD.: Спасибо за подсказку Eivind. Не учел в статье, что операторы присвоения должны возвращать референс на *this, а не значение. Это де-факто стандартное ожидаемое поведение.

То есть более правильным будет является такой код:
Vector3 operator = (Vector3 v1)
{
	this->x = v1.x, this->y = v1.y, this->z = 0;
	return *this;
}


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

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

    //Передача объекта по ссылке (&v1)
    Vector3 operator = (Vector3 &v1)
    {
    	return Vector3(this->x = v1.x, this->y = v1.y, this->z = 0);
    }
    

    Когда мы передаем объект не по ссылке, то по сути создается новый объект (копия того, который передается в метод), что может нести за собой определенные издержки как по времени выполнения программы, так и по потребляемой ей памяти. — Применительно к большим объектам.

    Передавая объект по ссылке, не происходит выделения памяти под сам объект (предположим, 128 байт) и операции копирования, память выделяется лишь под указатель на ячейку памяти, с которой мы работаем, а это около 4 — 8 байт. Таким образом, получается работа с объектом на прямую.

  • Но, если мы передаем объект по ссылке, то он становится изменяемым. То есть ничто не помешает нам при операции присваивания (v1 = v2) изменять не только значение v1, но еще и v2!

    Пример:

    //Изменение передаваемого объекта
    Vector3 operator = (Vector3 &v)
    {
    	//Меняем объект, который справа от знака =
    	v.x = 10; v.y = 50;
    	//Возвращаем значение для объекта слева от знака =
    	return Vector3(this->x = v.x, this->y = v.y, this->z = 0);
    }
    

    Разумеется, вряд ли кто-то в здравом уме станет производить такие не очевидные манипуляции. Но все же, не помешает исключить даже вероятность такого изменения.

    Для этого нам всего-лишь нужно добавить const перед принимаемым аргументом, таким образом мы укажем, что изнутри метода нельзя изменить этот объект.

    //Запрет изменения передаваемого объекта
    Vector3 operator = (const Vector3 &v)
    {
    	//Не получится изменить объект, который справа от знака =
    	//v.x = 10; v.y = 50;
    	//Возвращаем значение для объекта слева от знака =
    	return Vector3(this->x = v.x, this->y = v.y, this->z = 0);
    }
    

  • Теперь, давайте обратим наши взоры на тип возвращаемого значения. Метод перегрузки возвращает объект Vector3, то есть создается новый объект, что может приводить к таким же проблемам, которые я описал в самом первом пункте. И решение не будет отличаться оригинальностью, нам не нужно создавать новый объект — значит просто передаем ссылку на уже существующий.

    //Возвращается не объект, а ссылка на объект
    Vector3& operator = (const Vector3 &v)
    {
    	return Vector3(this->x = v.x, this->y = v.y, this->z = 0);
    }
    

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

    Мы уже не напишем такое выражение: v1 = (v2 + v3);

    Небольшое отступление о return:
    Когда я изучал перегрузки, то не понимал:

    //Зачем писать this->x = ... (что может приводить к ошибкам в бинарных операторах)
    return Vector3(this->x = v.x, this->y = v.y, this->z = 0);
    //Если мы все равно возвращаем объект с модифицированными данными? 
    //Почему такая запись не будет работать? (Применительно к унарным операторам)
    return Vector3(v.x, v.y, 0);
    

    Дело в том, что все операции мы должны самостоятельно и явно указать в теле метода. Что значит, написать: this->x = v.x и т.д.

    Но для чего тогда return, что мы возвращаем? На самом деле return в этом примере играет достаточно формальную роль, мы вполне можем обойтись и без него:

    //Возвращается void (ничего)
    void operator = (const Vector3 &v1)
    {
    	this->x = v1.x, this->y = v1.y, this->z = 0;
    }
    

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

    v1 = (v2 = v3);
    //Пример для void operator +
    //v1 = void? - Нельзя
    v1 = (v2 + v3);
    

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

    Получается, что лучше возвращать объект а не ссылку? Не все так однозначно, и выбирать тип возвращаемого значения (объект или ссылка) необходимо в каждом конкретном случае. Но для большинства небольших объектов — лучше возвращать сам объект, чтобы мы имели возможность дальнейшей работы с результатом.

    Отступление 2 (как делать не нужно):
    Теперь, зная о разнице операции return и непосредственного выполнения операции, мы можем написать такой код:

    v1(10, 10, 10);
    v2(15, 15, 15);
    v3;
    
    v3 = (v1 + v2);
    
    cout << v1; // Не (10, 10, 10), а (12, 13, 14)
    cout << v2; // Не (15, 15, 15), а (50, 50, 50)
    cout << v3; // Не (25, 25, 25), а также, что угодно
    

    Для того, что бы реализовать этот ужас мы определим перегрузку таким образом:

    Vector3 operator + (Vector3 &v1, Vector3 &v2)
    {
    	v1.x += 2, v1.y += 13, v1.z += 4;
    	v2(50, 50, 50);
    	return Vector3(/*также, что угодно*/);
    }
    

  • И когда мы перегружаем оператор присваивания, остается необходимость исключить попеременное присваивание в том редком случае, когда по какой-то причине объект присваивается сам себе: v1 = v1.
    Для этого добавим такое условие:

    Vector3 operator = (const Vector3 &v1)
    {
    	//Если попытка сделать объект равным себе же, просто возвращаем указатель на него
    	//(или можно выдать предупреждение/исключение)
    	if (&v1 == this)
    		return *this;
    	return Vector3(this->x = v1.x, this->y = v1.y, this->z = v1.z);
    }
    


Отличия унарных и бинарных операторов


Унарные операторы — это такие операторы, где задействуется только один объект, к которому и применяются все изменения

Vector3 operator + (const Vector3 &v1); // Унарный плюс
Vector3 operator - (const Vector3 &v1); // Унарный минус
//А так же:
//++, --, !, ~, [], *, &, (), (type), new, delete

Бинарные операторы — работают с 2-я объектами

Vector3 operator + (const Vector3 &v1, const Vector3 &v2); //Сложение - это НЕ унарный плюс!
Vector3 operator - (const Vector3 &v1, const Vector3 &v2); //Вычитание - это НЕ унарный минус!
//А так же:
//*, /, %, ==, !=, >, <, >=, <=, &&, ||, &, |, ^, <<, >>, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, ->, ->*, (,), ","

Перегрузка в теле и за телом класса


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

struct Vector3
{
	//Данные, конструкторы, ...
	//Объявляем о том, что в данной структуре перегружен оператор =
	Vector3 operator = (Vector3 &v1);
};
//Реализуем перегрузку за пределами тела структуры
//Для этого добавляем "Vector3::", что указывает на то, членом какой структуры является перегружаемый оператор
//Первая надпись Vector3 - это тип возвращаемого значения
Vector3 Vector3::operator = (Vector3 &v1);
{
	return Vector3(this->x = v1.x, this->y = v1.y, this->z = 0);
}

Зачем в перегрузке операторов дружественные функции (friend)?


Дружественные функции — это такие функции которые имеют доступ к приватным методам класса или структуры.

Предположим, что в нашей структуре Vector3, такие члены как x,y,z — являются приватными, тогда мы не сможем обратиться к ним за пределами тела структуры. Здесь то и помогают дружественные функции.
Единственное изменение, которое нам необходимо внести, — это добавить ключевое слово fried перед объявлением перегрузки:

struct Vector3
{
	friend Vector3 operator = (Vector3 &v1);
};
//За телом структуры пишем реализацию

Когда не обойтись без дружественных функций в перегрузке операторов?


1) Когда мы реализуем интерфейс (.h файл) в который помещаются только объявления методов, а реализация выносится в скрытый .dll файл

2) Когда операция производится над объектами разных классов. Пример:

struct Vector2
{
	//Складываем Vector2 и Vector3
	Vector2 operator + (Vector3 v3) {/*...*/}
}
//Объекту Vector2 присваиваем сумму объектов Vector2 и Vector3
vec2 = vec2 + vec3; //Ok
vec2 = vec3 + vec2; //Ошибка

Ошибка произойдет по следующей причине, в структуре Vector2 мы перегрузили оператор +, который в качестве значения справа принимает тип Vector3, поэтому первый вариант работает. Но во втором случае, необходимо писать перегрузку уже для структуры Vector3, а не 2. Чтобы не лезть в реализацию класса Vector3, мы можем написать такую дружественную функцию:

struct Vector2
{
	//Складываем Vector2 и Vector3
	Vector2 operator + (Vector3 v3) {/*...*/}
	//Дружественность необходима для того, чтобы мы имели доступ к приватным членам класса Vector3
	friend Vector2 operator + (Vector3 v3, Vector2 v2) {/*...*/}
}
vec2 = vec2 + vec3; //Ok
vec2 = vec3 + vec2; //Ok



Примеры перегрузок различных операторов с некоторыми пояснениями


Пример перегрузки для бинарных +, -, *, /, %

Vector3 operator + (const Vector3 &v1, const Vector3 &v2)
{
	return Vector3(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z);
}

Пример перегрузки для постфиксных форм инкремента и декремента (var++, var--)

Vector3 Vector3::operator ++ (int)
{
	return Vector3(this->x++, this->y++, this->z++);
}

Пример перегрузки для префиксных форм инкремента и декремента (++var, --var)

Vector3 Vector3::operator ++ ()
{
	return Vector3(++this->x, ++this->y, ++this->z);
}

Перегрузка арифметических операций с объектами других классов

Vector3 operator * (const Vector3 &v1, const int i)
{
	return Vector3(v1.x * i, v1.y * i, v1.z * i);
}

Перегрузка унарного плюса (+)

//Ничего не делает, просто возвращаем объект
Vector3 operator + (const Vector3 &v)
{
	return v;
}

Перегрузка унарного минуса (-)

//Умножает объект на -1
Vector3 operator - (const Vector3 &v)
{
	return Vector3(v.x * -1, v.y * -1, v.z * -1);
}

Пример перегрузки операций составного присваивания +=, -=, *=, /=, %=

Vector3 operator += (Vector3 &v1, const Vector3 &v2)
{
	return Vector3(v1.x = v1.x + v2.x, v1.y = v1.y + v2.y, v1.z = v1.z + v2.z);
}

Хороший пример перегрузки операторов сравнения ==, !=, >, <, >=, <=

const bool operator < (const Vector3 &v1, const Vector3 &v2)
{
	double vTemp1(sqrt(pow(v1.x, 2) + pow(v1.y, 2) + pow(v1.z, 2)));
	double vTemp2(sqrt(pow(v2.x, 2) + pow(v2.y, 2) + pow(v2.z, 2)));

	return vTemp1 < vTemp2;
}
const bool operator == (const Vector3 &v1, const Vector3 &v2)
{
	if ((v1.x == v2.x) && (v1.y == v2.y) && (v1.z == v2.z))
		return true;
	return false;
}
//Перегружаем != используя другой перегруженный оператор
const bool operator != (const Vector3 &v1, const Vector3 &v2)
{
	return !(v1 == v2);
}

Пример перегрузки операций приведения типов (type)

//Если вектор не нулевой - вернуть true
Vector3::operator bool() const
{
	if (*this != Vector3(0, 0, 0))
		return true;
	return false;
}
//При приведении к типу int - возвращать сумму всех переменных
Vector3::operator int() const
{
	return int(this->x + this->y + this->z);
}

Пример перегрузки логических операторов !, &&, ||

//Опять же, используем уже перегруженную операцию приведения типа к bool
const bool operator ! (Vector3 &v1)
{
	return !(bool)v1;
}
const bool operator && (Vector3 &v1, Vector3 &v2)
{
	return (bool)v1 && (bool)v2;
}

Пример перегрузки побитовых операторов ~, &, |, ^, <<, >>

//Операция побитовой инверсии (как умножение на -1, только немного иначе)
const Vector3 operator ~ (Vector3 &v1)
{
	return Vector3(~(v1.x), ~(v1.y), ~(v1.z));
}
const Vector3 operator & (const Vector3 &v1, const Vector3 &v2)
{
	return Vector3(v1.x & v2.x, v1.y & v2.y, v1.z & v2.z);
}
//Побитовое исключающее ИЛИ (xor)
const Vector3 operator ^ (const Vector3 &v1, const Vector3 &v2)
{
	return Vector3(v1.x ^ v2.x, v1.y ^ v2.y, v1.z ^ v2.z);
}
//Перегрузка операции вывода в поток
ostream& operator << (ostream &s, const Vector3 &v)
{
	s << '(' << v.x << ", " << v.y << ", " << v.z << ')';
	return s;
}
//Перегрузка операции ввода из потока (очень удобный вариант)
istream& operator >> (istream &s, Vector3 &v)
{
	std::cout << "Введите Vector3.\nX:";
	std::cin >> v.x;
	std::cout << "\nY:";
	std::cin >> v.y;
	std::cout << "\nZ:";
	std::cin >> v.z;
	std::cout << endl;
	return s;
}

Пример перегрузки побитного составного присваивания &=, |=, ^=, <<=, >>=

Vector3 operator ^= (Vector3 &v1, Vector3 &v2)
{
	v1(Vector3(v1.x = v1.x ^ v2.x, v1.y = v1.y ^ v2.y, v1.z = v1.z ^ v2.z));
	return v1;
}
//Предварительно очищаем поток
ostream& operator <<= (ostream &s, Vector3 &v)
{
	s.clear();
	s << '(' << v.x << ", " << v.y << ", " << v.z << ')';
	return s;
}

Пример перегрузки операторов работы с указателями и членами класса [], (), *, &, ->, ->*
Не вижу смысла перегружать (*, &, ->, ->*), поэтому примеров ниже не будет.

//Не делайте подобного! Такая перегрузка [] может ввести в заблуждение, это просто пример реализации
//Аналогично можно сделать для ()
int Vector3::operator [] (int n)
{
	try
	{
		if (n < 3)
		{
			if (n == 0)
				return this->x;
			if (n == 1)
				return this->y;
			if (n == 2)
				return this->z;
		}
		else
			throw "Ошибка: Выход за пределы размерности вектора";
	}
	catch (char *str)
	{
		cerr << str << endl;
	}
	return NULL;
}
//Этот пример также не имеет практического смысла
Vector3 Vector3::operator () (Vector3 &v1, Vector3 &v2)
{
	return Vector3(v1 & v2);
}

Как перегружать new и delete? Примеры:

//Выделяем память под 1 объект
void* Vector3::operator new(size_t v)
{
	void *ptr = malloc(v);
	if (ptr == NULL)
		throw std::bad_alloc();
	return ptr;
}
//Выделение памяти под несколько объектов
void* Vector3::operator new[](size_t v)
{
	void *ptr = malloc(sizeof(Vector3) * v);
	if (ptr == NULL)
		throw std::bad_alloc();
	return ptr;
}
void Vector3::operator delete(void* v)
{
	free(v);
}
void Vector3::operator delete[](void* v)
{
	free(v);
}

Перегрузка new и delete отдельная и достаточно большая тема, которую я не стану затрагивать в этой публикации.

Перегрузка оператора запятая ,

Внимание! Не стоит путать оператор запятой с знаком перечисления! (Vector3 var1, var2;)

const Vector3 operator , (Vector3 &v1, Vector3 &v2)
{
	return Vector3(v1 * v2);
}

v1 = (Vector3(10, 10, 10), Vector3(20, 25, 30));
// Вывод: (200, 250, 300)

Источники


1) https://ru.wikipedia.org/wiki/Операторы в C и C++
2) Р. Лафоре Объектно-Ориентированное Программирование в С++
3) Спасибо всем за комментарии к публикации и указания на недочеты!
Поделиться с друзьями
-->

Комментарии (69)


  1. Shtucer
    31.08.2016 11:41
    +7

    «всех 49-и» когда же это кончится-то?

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


    Моей первой книгой по C++ была «Язык программирования C++» Бьёрна Страуструпа. Тема перегрузки операторов там раскрыта была понятно и чуть меньше, чем полностью… Если какие вопросы и оставались, то очень частные. С++ со своими новыми стандартами, боюсь, убежал от меня очень далеко, поэтому поинтересуюсь: Страуструпа начинающим сейчас рекомендовать уже моветон?


    1. RomanArzumanyan
      31.08.2016 11:45
      -12

      Боюсь, что С++ рекомендовать уже моветон (сам пишу на С++/С, с каждым стандартом становится всё страшнее).


      1. charypopper
        31.08.2016 19:41

        Не пользуйтесь плоскогубцами, потому что комитет каждый раз делает стандарт, по которому инструмент все страшнее


        1. RomanArzumanyan
          01.09.2016 00:38

          Окей, давайте сыграем в аналогии.
          Были плоскогубцы (С). Из них сделали мультитул (С++ 98) — добавили нож и пару отвёрток. Вроде по уму, пользоваться удобно. А потом зачем-то флэшку, лазерную указку, лупу, пилку для ногтей, крючок для почтовых посылок и ещё чёрте-что (С++ 17).
          Это несмотря на то, что пользователи просят кусачки-бокорезы и открывашку для пива (модули и AST).
          А если нужны конкретно плоскогубцы, чтобы каждый день ими работать, то покупаем конкретно их (привет, С).

          Так что вы с плоскогубцами привели прекрасный пример, КМК.


    1. vt4a2h
      31.08.2016 12:17
      +3

      Все зависит от уровня и целей начинающего. У Страуструпа теперь есть и книга попроще, для новичков.
      А новые стандарты (11/14) просто замечательны и делают жизнь намного проще. Правда вот 17ый разочаровал отсуствием модулей.


    1. jah_lives_in_me
      31.08.2016 19:44

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


      1. torkve
        31.08.2016 19:51
        +3

        Учтите, что вы им вредите этой статьёй.


        1. jah_lives_in_me
          31.08.2016 20:18

          Буду очень благодарен если Вы объективно укажете на ошибки или недочеты в статье.


          1. torkve
            31.08.2016 20:23

            Я давно это сделал чуть ниже.
            Главная ошибка этой статьи заключается в том, что она подталкивает гипотетическую ЦА к имплементации операторов с ошибками и там, где это совершенно не нужно.


            1. jah_lives_in_me
              31.08.2016 20:39

              Я ответил Вам ниже. Спасибо за указание на недочет!


      1. Shtucer
        31.08.2016 19:53
        +2

        для тех людей, которые задаются вопросами о перегрузке, но пока не дошли до Страуструпа


        Да, статью написать это одно, а выдумать для неё целевую аудиторию это другое…


      1. Chaos_Optima
        01.09.2016 17:03

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

        //Возвращается не объект, а ссылка на объект
        Vector3& operator = (const Vector3 &v)
        {
        	return Vector3(this->x = v.x, this->y = v.y, this->z = 0); 
        }
        


  1. Eivind
    31.08.2016 11:46
    +11

    Корректные операторы присвоения должны возвращать референс на *this, а не значение. Это де-факто стандартное ожидаемое поведение.


    1. hdfan2
      31.08.2016 11:49
      +1

      Это если они члены класса. Кстати, про это тут, как я понял, ни слова.


      1. Eivind
        31.08.2016 11:52
        +2

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


        1. hdfan2
          31.08.2016 11:57
          +1

          Да, ошибся. Тут всё вперемешку.


    1. zagayevskiy
      31.08.2016 11:58
      +8

      То же самое про инкременты/декременты. Вообще, кг/ам, имхо. Автору сначала стоит попрактиковаться в С++, а потом уже пытаться писать статьи.


  1. torkve
    31.08.2016 11:56
    +13

    > Vector3& operator = (const Vector3 &v)
    > Vector3 operator = (const Vector3 &v)

    Вы творите что-то страшное. Ущербность кода

    Vector3& operator = (const Vector3 &v) {
      ...
      return Vector3(...);
    

    вы поняли, но сделали из этого в корне неверный вывод: возвращать rvalue (новый объект).
    Согласно негласным общепринятым правилам, результат подобных модифицирующих операций (=, +=, ++ и т.п.) можно присваивать. Но присваивать не только новому объекту, но и ссылке:
    Vector3& a = (b += c);
    

    Чисто из семантики это означает, что мы модифицировали b, и создали ссылку на b, т.е. c и b указывают на один и тот же объект, Это означает, что если я сделаю a.x = 42, то и b.x будет равен тому же. Вы же возвращаете новый объект, и такое присвоение невозможно, а даже если бы и удалось, то в итоге a и b указывали бы на разные объекты с одним и тем же значением.
    Такие операторы корректно писать так:
    Vector3& operator = (const Vector3 &other)
    {
        if (this != &other) // если кто-то напишет "a = a;", лучше всего ничего не делаеть
        {
             x = other.x;
             y = other.y;
             z = other.z;
        }
        return *this;
    }
    

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


    1. dymanoid
      31.08.2016 16:11
      +4

      Только не c и b будут указывать на один и тот же объект, а a и b.


      1. torkve
        31.08.2016 17:57
        +1

        Ой, да, конечно же, я опечатался. Спасибо.


    1. rkfg
      31.08.2016 16:29
      +3

      Я вообще не очень понимаю, как можно возвращать ссылку на локальную переменную, хоть она и rvalue (в первом случае), это ведь UB, не? Автор создаёт новый объект класса Vector3 и возвращает его по ссылке, но т.к. это автоматическая переменная, она тут же будет удалена при завершении функции, и ссылка в общем случае будет невалидной. По значению возвращать можно без проблем, конечно, тут ещё и копирования не будет за счёт copy elision.

      Вообще, перегрузка оператора присвоения — это нетривиальная тема, в отличие от большинства других операторов, т.к. мало просто скопировать данные, нужно ещё и старое значение переменной корректно очистить. На SO очень хорошо описан по шагам путь к элегантной идиоме copy-and-swap: http://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom


      1. torkve
        31.08.2016 17:58
        +2

        > Я вообще не очень понимаю, как можно возвращать ссылку на локальную переменную, хоть она и rvalue (в первом случае), это ведь UB, не?
        Никак нельзя конечно. Автор собственно об этом и пишет и поэтому принимает решение изменить сигнатуру оператора, чтобы он возвращал rvalue вместо ссылки.


    1. nolane
      31.08.2016 19:47

      Зачастую, и конкретно в этом коде, присваивание себя себе не является особым случаем. Кроме того, "A copy assignment operator that is written in such a way that it must check for self-assignment is probably not strongly exception-safe either" Herb Sutter's book Exceptional C++ (1999). Подробнее.


      1. torkve
        31.08.2016 19:48

        Да, безусловно. Для более сложных случаев нужен std::swap и всё в этом духе.


    1. jah_lives_in_me
      31.08.2016 20:29

      Применительно к оператору присвоения все верно — Вы модифицируете сам объект и можете вернуть на него ссылку. Но такой подход не будет работать при перегрузке бинарных операторов. Похоже, что я не очень удачно выбрал оператор "=" для демонстрации различных тонкостей перегрузок.


      1. torkve
        31.08.2016 20:45
        +1

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

        P.s. За сигнатуру

        const bool operator ! (Vector3 &v1) {}
        я бы повесил вас на сосне.


        1. jah_lives_in_me
          31.08.2016 20:51

          «void или ссылки на результат присвоения возвращаете другой новый объект» — для оператора присвоения есть пример с void. Спасибо Вам за здравую критику, Вы все пишете верно! Впредь буду более внимателен при составлении примеров.


  1. kokorins
    31.08.2016 12:15
    -2

    * или -> переопределить нельзя какие то еще базовые для работы с указателями тоже. еще почему не стоит переопределять скобки квадратные и круглые?


    1. sborisov
      31.08.2016 12:18
      +4

      А как по вашему работаю smart pointers? Если нельзя перезагружать, как в ыговорите эти операторы?


    1. sborisov
      31.08.2016 12:20
      +2

      Следующие- операторы не могут быть определены пользователем:
      :: (разрешение области видимости; § 4.9.4, § 10.2.4);
      . (выбор члена; § 5.7);
      .* (выбор члена через указатель на член; § 15.5).
      Правым операндом у них является не значение, а имя, и они предоставляют основные
      средства доступа к членам. Разрешение их перегрузки привело бы к очень тонким ошиб-
      ошибкам [Stroustrup, 1994]. Тернарный условный оператор ?: также не может быть перегру-
      перегружен. Аналогично не могут быть прегружены операторы sizeof(§ 14.6) и typeid(§ 15.4.4).


  1. sborisov
    31.08.2016 12:17
    +2

    В общем, лучше новичкам, прочитать 11 главу от старика Бьёрна. Там очень всё понятно расписано.


  1. Tujh
    31.08.2016 12:32
    +8

    Не вижу смысла перегружать (*, &, ->, ->*)
    То есть все умные указатели С++11/14 не имеют смысла?


    1. jah_lives_in_me
      31.08.2016 19:48
      -4

      Умные указатели уже являются контейнером STL, «не вижу смысла» — делать это самостоятельно


      1. Andropolon
        01.09.2016 12:26

        Говорят, что Unreal Engine 4 использует перегрузку в классе UEngine для жесткой проверки обращения только из основного потока
        Кажется, что иногда смысл есть.


      1. Tujh
        01.09.2016 13:20

        Я приведу простой пример, хотя он и достаточно специфичен.

        Поищите по словам «embedded STL».
        Во многом проблема ещё усугубляется тем, что для подобного оборудования реализованы компиляторы только с поддержкой С++98, и даже boost не удаётся использовать (я сейчас не об Embedded Linux, а о «deep embedded»).
        И да, споры о том, уместен ли С++ для подобных устройств, или «pure C only» давайте оставим за рамками обсуждения.


        1. torkve
          01.09.2016 14:58

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


      1. Satus
        01.09.2016 19:30

        А если нужно написть свой умный указатель?


  1. DistortNeo
    31.08.2016 14:39
    +4

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

    Оператор «запятая» лучше вообще никогда не трогать. Хотя бы потому, что при его перегрузке пропадает точка следования (возможно, это изменится в C++17).


    1. jah_lives_in_me
      31.08.2016 19:50

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


    1. drozdVadym
      31.08.2016 23:42

      Удачный пример перегрузки оператора запятая есть в библиотеке Eigen

      eigen docs


      1. DistortNeo
        01.09.2016 00:04

        С появлением uniform initialization это стало уже не так актуально.
        Оператор битового сдвига я бы тоже не трогал, т.к. перегрузка меняет его семантику.


  1. tangro
    31.08.2016 16:26
    +1

    Вообще, перегрузка операторов имеет весьма ограниченное применение. Беда с ними в том, что для пользовательского класса ты не ожидаешь получить какое-то там особенно поведение по оператору сравнения или разименования указателя. Тот, кто будет этот код читать — не будет на это рассчитывать, совсем не сразу это поведение заметит и поймёт.

    Да, для всяких там векторов\матриц переопределение арифметики имеет смысл и даже удобно, поскольку читатель явно ожидает наличия таких операторов. Но такие классы лучше брать из готовых библиотек, чем писать самому — быстрее, надёжнее. Вот и получается, что в реальной жизни бывает нужно ну там оператор < может перегрузить для сортировок каких-нибудь, да и всё.


    1. rkfg
      31.08.2016 16:35
      +1

      Ну если действие применяется для объектов каких-то пользовательских классов, скорее всего, оператор перегружен. Хорошая IDE позволяет находить правильную перегрузку, если одной документации мало. А в целом, это как специя: применяй дозированно и только в нужных местах, будет вкусно; засыпь всё толстым слоем, потому что это же так прикольно и семантично (на взгляд автора), ну и отправится блюдо в мусорку.


  1. nikitaevg
    31.08.2016 16:28
    +4

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


  1. savostin
    31.08.2016 16:40
    +4

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

    Так и не рассказали.


    1. jah_lives_in_me
      31.08.2016 19:52

      Самый первый заголовок: «Для чего нужна перегрузка? Предположим, что вы создаете свой класс или структуру, пусть он будет описывать вектор в 3-х мерном пространстве… „


      1. saluev
        31.08.2016 20:14
        +1

        Это не объяснение. Почему бы для своего класса не вынести присваивание в отдельную функцию, с нормальным именем, например?


        1. jah_lives_in_me
          31.08.2016 20:31
          -3

          Об этом расказанно в видео


          1. saluev
            31.08.2016 23:42
            +3

            Мне казалось, мы на Хабре, а не на Youtube.


  1. DmitryLeonov
    31.08.2016 16:51
    +2

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


    1. jah_lives_in_me
      31.08.2016 20:32
      -1

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


      1. PkXwmpgN
        31.08.2016 21:23
        +1

        Вот это тоже не очень удачно


        Vector3 operator += (const Vector3 &v1, const Vector3 &v2)
        {
            return Vector3(v1.x = v1.x + v2.x, v1.y = v1.y + v2.y, v1.z = v1.z + v2.z);
        }


        1. jah_lives_in_me
          31.08.2016 21:26
          -3

          Да, я обратил на это внимание только тогда, когда уже записывал видео по перегрузке операторов: https://youtu.be/Qn6mu9l6Xj8

          Более удачным вариантом будет:

          Vector3& operator += (const Vector3 &v1, const Vector3 &v2)
          {
              v1.x = v1.x + v2.x, v1.y = v1.y + v2.y, v1.z = v1.z + v2.z;
              return *v1;
          }
          


          1. PkXwmpgN
            31.08.2016 21:38
            +2

            Нет. Вы пытаетесь присвоить значение константному объекту.


            1. jah_lives_in_me
              31.08.2016 22:05
              -1

              Спасибо, не заметил! В видео такой ошибки тоже нет.


              1. PkXwmpgN
                01.09.2016 16:27
                +1

                Я призываю вас внимательней отнестить к тому коду, который вы пишете


                То есть более правильным будет является такой код:

                Vector3 operator = (Vector3 v1)
                {
                    ...
                    return *this;
                }

                Нет. Более правильным будет такой код


                Vector3 & Vector3::operator = (const Vector3 & v1)
                {
                    ...
                    return *this;
                }

                Пример перегрузки для префиксных форм инкремента и декремента (++var, --var)

                Vector3 Vector3::operator++();

                Нет. Должно быть так


                Vector3 & Vector3::operator++();
                
                // или так
                Vector3 & operator++(Vector3 & v);

                Пример перегрузки операций составного присваивания +=, -=, *=, /=, %=

                Vector3 operator += (Vector3 &v1, const Vector3 &v2)
                {
                    return Vector3(v1.x = v1.x + v2.x, v1.y = v1.y + v2.y, v1.z = v1.z + v2.z);
                }

                Нет. Должно быть так


                Vector3 & Vector3::operator +=(const Vector3 & v);
                
                // или так
                Vector3 & operator +=(Vector3 & v1, const Vector3 & v2);

                Хороший пример перегрузки операторов сравнения ==, !=, >, <, >=, <=

                const bool operator < (const Vector3 &v1, const Vector3 &v2);

                Почему тип возвращаемого результата const bool, a не bool?


                Пример перегрузки логических операторов !, &&, ||

                Здесь следует отметить, что операторы && и || оболадают сементикой logical short-circuiting. И при перегрузке это свойство теряется.


                Пример перегрузки побитовых операторов ~, &, |, ^, <<, >>

                const Vector3 operator ~ (Vector3 &v1);

                Почему тип возвращаемого результата const Vector3, а не Vector3?


                Перегрузка операции ввода из потока (очень удобный вариант)

                istream& operator >> (istream &s, Vector3 &v)
                {
                    std::cout << "Введите Vector3.\nX:";
                    std::cin >> v.x;
                    std::cout << "\nY:";
                    std::cin >> v.y;
                    std::cout << "\nZ:";
                    std::cin >> v.z;
                    std::cout << endl;
                    return s;
                }

                Выглядит стронно, относительно объекта s.


                Пример перегрузки операторов работы с указателями и членами класса [], (), , &, ->, ->

                int Vector3::operator [] (int n)
                {
                    try
                    {
                        ...
                        throw "Ошибка: Выход за пределы размерности вектора";
                    }
                    catch (char *str)
                    {
                        ..
                    }
                    return 0;
                }

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


                try
                {
                    ...
                    throw "Ошибка: Выход за пределы размерности вектора";
                }
                catch (const char *str)
                {
                    ..
                }

                Также следует отметить, что оператор [], (), ->, могут быть только членами класса


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


                По поводу записи типа


                Vector3(this->x++, this->y++, this->z++)

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


      1. DmitryLeonov
        31.08.2016 22:13

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

        Уже второй фрагмент — полная дичь:

        Vector3 v1, v2, v3;
        //Инициализация
        v1(10, 10, 10); // вот это — не пропустит ни один компилятор, если не озаботиться перегрузкой еще и оператора ()


  1. eugene_e
    31.08.2016 19:52

    Удивился, когда увидел, что перегрузка ввода\вывода в поток не является friend. А автор просто объявил Vector3 как struct а не как class, соответственно, все члены public, и компилятор это проглатывает. А вообще, перегрузка ввода\вывода в поток должна быть дружественной (дружеской?) функцией. И еще замечение: this-x избыточно, достаточно просто написать x чтобы обратиться к члену класса. Ну и про то, что в операторах присваивания не надо создавать новый объект, а надо работать с текущим выше уже упоминали.


    1. jah_lives_in_me
      31.08.2016 19:53
      -1

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


      1. torkve
        31.08.2016 20:26

        А что за ключевое слово «жареный»? Оно у вас уже встречается второй раз, но я ни разу не слышал про жареные функции в C++…
        И где-то есть ещё и видео?!


        1. jah_lives_in_me
          31.08.2016 20:34

          Не заметил пропуск буквы :( Конечно же FRIEND. Да, ссылка на видео скоро будет в статье.


          1. rkfg
            31.08.2016 22:31
            +1

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

            По-моему, лучше вылизать одну статью, чем сделать десяток видео на ту же тему. Но это только моё мнение, конечно.


            1. DistortNeo
              31.08.2016 22:48
              +1

              Было бы неплохо, если бы данная публикация также не индексировалась до исправления всех допущенных в ней косяков. Новички найдут её и нагородят потом нехорошего.

              Вот ещё ошибочное место, на которое пока никто ещё не указал. Вместо

              Vector3::operator bool()

              следует писать

              Vector3::operator bool() const


              1. jah_lives_in_me
                31.08.2016 23:43

                Спасибо, исправлю.


            1. jah_lives_in_me
              31.08.2016 23:38

              Полностью согласен с Вами. Книги, лекции и статьи имеют гораздо большую ценность. Спасибо за критику, буду совершенствовать подготовку и написание статей.


  1. drozdVadym
    31.08.2016 23:34

    Зачем писать так много кода?

    const bool operator == (const Vector3 &v1, const Vector3 &v2)
    {
    	if ((v1.x == v2.x) && (v1.y == v2.y) && (v1.z == v2.z))
    		return true;
    	return false;
    }
    


    Намного же лучше так:

    const bool operator == (const Vector3 &v1, const Vector3 &v2)
    {
        return (v1.x == v2.x) && (v1.y == v2.y) && (v1.z == v2.z);
    }
    


  1. Deosis
    01.09.2016 08:41
    +2

    > //Перегрузка операции ввода из потока (очень удобный вариант)
    Особенно он *удобен* в случае, когда стандартные ввод/вывод перенаправлены в сокеты, а мы пытаемся прочитать вектор из файла.
    Перегрузка операторов — это как забивать гвозди кувалдой, одно неосторожное движение и можно остаться без пальцев.
    Например, && и || при перегрузке перестают быть ленивыми.


    1. dymanoid
      01.09.2016 11:24

      Тоже хотел упомянуть про перегрузку и ленивое вычисление.


  1. jah_lives_in_me
    01.09.2016 09:33

    когда стандартные ввод/вывод перенаправлены в сокеты
    — Для этого указывается с каким потоком идет работа.