Что нужно возвращать из оператора присваивания?



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



Коротенький рекап. Операторы присваивания нужны для перенятия свойств других объектов. В общем случае даже объектов других классов. Но нас интересуют 2 особенных члена классов - копирующий и перемещающий операторы присваивания. Первый нужен, что скопировать содержимое одного объекта в другой и по договоренности обычно принимает константную lvalue ссылку на объект того же класса. Второй нужен для перемещения ресурсов из одного объекта в другой и принимает rvalue reference на объект того же класса.



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



Во всем посте дальше я буду писать про копирующий оператор, чтобы не распыляться, но несложно будет перейти к логике перемещающего оператора. Но в начале разберемся с семантикой. Я, как пользователь класса, ожидаю, что объект из которого я копирую не изменится, после выполнения оператора два объекта будут семантически идентичны, и чтобы я с объектом не сделал внутри одной строчки, он не потеряет свое новоприобретенное значение(если я явно не вызову std::move). С этим определились, поехали разбирать кейсы.



1️⃣ Можем вернуть объект вообще другого класса. Семантика такого решения будет самой неочевидной для пользователей класса. И хоть я могу представить что-то подобное для других операторов, например, если вычесть друг из друга 2 даты, то получим какое-то число, а не дату. Но для оператора присваивания я даже кейса никакого не могу привести, поэтому сразу скипаем этот вариант.



2️⃣ Можем вернуть объект по rvalue reference. В этом случае я могу потерять новоприобретенные ресурсы, если передам результат оператора в функцию, которая принимает аргумент по значению или по rvalue reference.



struct Class {

Class(int num) : a{num} {std::cout << "Ctor" << "n";}

Class&& operator=(const B& other) { a = other.a; return std::move(*this);}

Class(Class&& other) {a = other.a; other.a = 0;}

int a;

};



void func(Class obj) {

std::cout << obj.a << "n";

}



int main() {

Class a{2}, b{3};

func(b = a);

std::cout << b.a << "n";

}



//OUTPUT:

Ctor

Ctor

2

0




Это нарушает ожидаемую семантику поведения копирующего оператора, поэтому тоже не подходит.



3️⃣ Можем вернуть объект по значению. Основной и решающий, на мой взгляд, недостаток - это снижение производительности на лишнее копирование объекта и его удаление.



struct Class {

Class(int num) : a{num} {std::cout << "Ctor" << "n";}

Class(const Class& other) {a = other.a; std::cout << "Copy Ctor" << "n";}

Class operator=(const Class& other) { a = other.a; std::cout << "Copy Assign" << "n"; return *this;}

Class(Class&& other) {a = other.a; other.a = 0;}

~Class() {std::cout << "Dtor" << "n";}

int a;

};



int main() {

Class a{2}, b{3}, c{4};

c = b = a;

std::cout << a.a << " " << b.a << " " << c.a << "n";

}



//OUTPUT:

Ctor

Ctor

Ctor

Copy Assign

Copy Ctor

Copy Assign

Copy Ctor

Dtor

Dtor

2 2 2

Dtor

Dtor

Dtor


В итоге мы видим 2 лишних копирования и 2 лишних вызова деструктора. Этого достаточно, чтобы отбросить вариант. Лишних я говорю, потому что знаю просто, что их можно избежать. Просто даже по виду выражения c = b = a; видно, что тут просто 2 присваивания. И я не ожидаю, что вылезет еще какое-то копирование. Если неосознанно пользоваться таким оператором, то можно снижать перфоманс приложения, даже не осознавая этого.



Остальные пункты в один пост не влезет. Придется выделить во вторую часть.



Stay conscious about small things. Stay cool.



#cppcore