Operatorul de atribuire prin mutare
Operatorul de atribuire prin mutare (move assignment operator) are rolul de a rezolva aceleași probleme ca și constructorul de mutare. Acest operator are următoarea formă:
MyClass& operator=(MyClass&& moved)
{
// codul operatorului
return *this; // returnăm obiectul curent
}
Ca parametru, primește obiectul care urmează să fie mutat, printr-o referință rvalue. În codul operatorului se realizează operațiile necesare.
Să definim și să folosim operatorul de atribuire prin mutare:
#include <iostream>
// clasa mesaj
class Message
{
public:
// constructor normal
Message(const char* data, unsigned count)
{
size = count;
text = new char[size]; // alocăm memorie
for(unsigned i{}; i < size; i++) // copiem datele
{
text[i] = data[i];
}
id = ++counter;
std::cout << "Create Message " << id << std::endl;
}
// operator normal de atribuire
Message& operator=(const Message& copy)
{
std::cout << "Copy assign message " << copy.id << " to " << id << std::endl;
if (© != this) // evităm auto-atribuirea
{
delete[] text; // eliberăm memoria obiectului curent
// copiem datele din obiectul sursă în obiectul curent
size = copy.size;
text = new char[size]; // alocăm memorie
for(unsigned i{}; i < size; i++) // copiem datele
{
text[i] = copy.text[i];
}
}
return *this; // returnăm obiectul curent
}
// operator de atribuire prin mutare
Message& operator=(Message&& moved)
{
std::cout << "Move assign message " << moved.id << " to " << id << std::endl;
if (&moved != this) // evităm auto-atribuirea
{
delete[] text; // eliberăm memoria obiectului curent
text = moved.text; // copiem pointerul din obiectul mutat în cel curent
size = moved.size;
moved.text = nullptr; // resetăm pointerul din obiectul mutat
moved.size = 0;
}
return *this; // returnăm obiectul curent
}
// destructor
~Message()
{
std::cout << "Delete Message " << id << std::endl;
delete[] text; // eliberăm memoria
}
char* getText() const { return text; }
unsigned getSize() const { return size; }
unsigned getId() const {return id;}
private:
char* text{}; // textul mesajului
unsigned size{}; // dimensiunea mesajului
unsigned id{}; // numărul mesajului
static inline unsigned counter{}; // contor static pentru generarea numărului obiectului
};
int main()
{
char text1[] {"Hello Word"};
Message hello{text1, std::size(text1)};
char text2[] {"Hi World!"};
hello = Message{text2, std::size(text2)}; // atribuirea obiectului
std::cout << "Message " << hello.getId() << ": " << hello.getText() << std::endl;
}
Explicații succinte:
- Operatorul normal de atribuire copiază conținutul obiectului sursă în obiectul curent, alocând memorie nouă și copierea datelor
- Operatorul de atribuire prin mutare "mută" datele, preluând pointerul la memorie din obiectul sursă și resetând pointerul din sursă pentru a evita eliberarea dublă a memoriei
- Astfel se evită costurile copieri inutile și alocarea repetată a memoriei
În operatorul de atribuire, primim obiectul mutabil Message, eliberăm memoria alocată anterior și copiem valoarea pointerului din obiectul mutat:
Message& operator=(Message&& moved)
{
std::cout << "Move assign message " << moved.id << " to " << id << std::endl;
if (&moved != this) // evităm auto-atribuirea
{
delete[] text; // eliberăm memoria obiectului curent
text = moved.text; // copiem pointerul din obiectul mutat în cel curent
size = moved.size;
moved.text = nullptr; // resetăm pointerul din obiectul mutat
moved.size = 0;
}
return *this; // returnăm obiectul curent
}
În funcția main atribuim variabilei hello un obiect Message:
char text2[] {"Hi World!"};
hello = Message{text2, std::size(text2)};
Trebuie remarcat că, la fel ca și în cazul constructorului de mutare, valoarea atribuită este un rvalue — un obiect temporar în memorie (Message{text2, std::size(text2)}), care după operația de atribuire nu mai este necesar. Acesta este tocmai cazul ideal pentru aplicarea operatorului de atribuire prin mutare. Output-ul programului este:
Create message 1
Create message 2
Move assign message 2 to 1
Delete message 2
Message 1: Hi World!
Delete message 1
După cum se vede, variabila hello reprezintă obiectul Message cu numărul 1. Dacă în clasă sunt definiți mai mulți operatori de atribuire (cel standard și cel prin mutare), implicit pentru rvalue va fi folosit operatorul de atribuire prin mutare. La atribuirea unui lvalue se va folosi operatorul standard (fără mutare):
Message hello{"Hello Word", 11};
Message hi{"Hi World!", 10};
hello = hi; // atribuirea lvalue - operatorul standard de atribuire
hello = Message{"Hi World!", 10}; // atribuirea rvalue - operatorul de atribuire prin mutare
De asemenea, putem folosi funcția std::move() pentru a transforma un lvalue în rvalue:
Message hello{"Hello Word", 11};
Message hi{"Hi World!", 10};
hello = std::move(hi); // transformare lvalue în rvalue - operatorul de atribuire prin mutare
Aici variabila hi este transformată în rvalue, deci operatorul de atribuire prin mutare va fi apelat.
Compilatorul generează implicit operatorul de atribuire prin mutare, care mută valorile tuturor variabilelor nestatice. Însă dacă definim manual destructorul, constructorul de copiere, constructorul de mutare sau operatorul de atribuire, compilatorul nu mai generează operatorul de atribuire prin mutare implicit.
std::unique_ptr și mutarea valorilor
Pentru că pointerul inteligent std::unique_ptr indică în mod unic o adresă de memorie, nu pot exista două sau mai multe pointere std::unique_ptr care să indice aceeași zonă de memorie. Din acest motiv, tipul unique_ptr nu are constructor de copiere și operator de atribuire prin copiere. Dacă încercăm să le folosim, vom primi erori de compilare:
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> one{ std::make_unique<int>(123) };
std::unique_ptr<int> other;
// other = one; // Eroare! operatorul de atribuire prin copiere nu există
// std::unique_ptr<int> another{ other }; // Eroare! constructorul de copiere nu există
}
În schimb, unique_ptr are constructor de mutare și operator de atribuire prin mutare, pe care le putem folosi pentru a muta datele dintr-un pointer în altul:
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> one{ std::make_unique<int>(123) };
std::unique_ptr<int> other;
other = std::move(one); // operator de atribuire prin mutare
// std::cout << *one << std::endl; // datele din 'one' au fost mutate în 'other', deci aici ar fi invalid
std::cout << *other << std::endl; // 123
std::unique_ptr<int> another{ std::move(other) }; // constructor de mutare
std::cout << *another << std::endl; // 123
}
După mutarea valorii dintr-un pointer, nu mai putem accesa valoarea prin pointerul inițial.