Idiomă Move-and-Swap / Mutare cu schimb
Idiomul move-and-swap sau mutarea cu schimb este utilizat în operatorii de atribuire cu mutare. Acesta ajută la evitarea duplicării codului din destructor și constructorul de copiere. Esența acestei idiome este următoarea:
- Pentru obiectul care este mutat, creăm o copie folosind constructorul de mutare
- Înlocuim obiectul curent cu copia modificată. Dacă în timpul modificării copiei apare o eroare, obiectul curent nu este înlocuit
Forma generală a idiomei move-and-swap arată astfel:
MyClass& MyClass::operator=(MyClass&& rhs) noexcept
{
MyClass moved(std::move(rhs)); // obținem obiectul mutat
swap(moved); // efectuăm schimbul de valori
return *this; // returnăm obiectul curent
}
Să analizăm o implementare simplă cu un exemplu:
#include <iostream>
class Message {
public:
Message(std::string data) : text{new std::string(data)} { // alocăm memorie
id = ++counter;
std::cout << "Create message " << id << std::endl;
}
// constructor de copiere
Message(const Message& copy) : Message{copy.getText()} {
std::cout << "Copy message " << copy.id << " to " << id << std::endl;
}
// constructor de mutare
Message(Message&& moved) noexcept : text{moved.text} {
id = ++counter;
std::cout << "Move message " << moved.id << " to " << id << std::endl;
moved.text = nullptr;
}
~Message() {
std::cout << "Delete message " << id << std::endl;
delete text; // eliberăm memoria
}
// atribuire prin copiere
Message& operator=(const Message& copy) {
std::cout << "Copy assign message " << copy.id << " to " << id << std::endl;
if (© != this) { // evităm auto-atribuirea
*text = copy.getText();
}
return *this;
}
// atribuire prin mutare
Message& operator=(Message&& moved) noexcept {
std::cout << "Move assign message " << moved.id << " to " << id << std::endl;
Message temp{std::move(moved)}; // apelăm constructorul de mutare
swap(temp); // schimbăm valorile
return *this; // returnăm obiectul curent
}
// funcția de schimb
void swap(Message& other) noexcept {
std::swap(text, other.text); // schimbăm cele două pointere
}
std::string& getText() const { return *text; }
unsigned getId() const { return id; }
private:
std::string* text; // textul mesajului
unsigned id{}; // numărul mesajului
static inline unsigned counter{}; // contor static pentru generarea numărului obiectului
};
int main() {
Message mes{""};
mes = Message{"hello"}; // atribuire prin mutare
std::cout << "Message " << mes.getId() << ": " << mes.getText() << std::endl;
}
Explicația programului:
1) În constructorul Message, se alocă memorie dinamică pentru a stoca un obiect de tipul std::string, iar în destructor, memoria este eliberată.
2) Se creează un nou obiect folosind constructorul de copiere și se copiază datele. Aici se folosește delegarea constructorului pentru a evita duplicarea logicii.
3) Se mută pointerul la text dintr-un obiect într-altul. După mutare, obiectul sursă este setat la nullptr pentru a preveni utilizarea incorectă a memoriei.
4) Operatorul de atribuire copiază datele dintr-un alt obiect, iar înainte de a face acest lucru, verifică dacă obiectul curent este diferit de obiectul care trebuie copiat (evitând auto-atribuirea).
5) În acest operator, se folosește idiomul move-and-swap. Se creează un obiect temporar folosind constructorul de mutare și apoi se face schimbul între obiectul curent și cel temporar. Dacă ceva nu merge bine, obiectul curent nu este afectat.
6) Această funcție schimbă pur și simplu două pointere, ceea ce permite schimbul valorilor între două obiecte.
Ieșirea programului:
Create message 1
Create message 2
Move assign message 2 to 1
Move message 2 to 3
Delete message 3
Delete message 2
Message 1: hello
Delete message 1
Explicație:
1) Obiectul mes este creat cu textul gol.
2) Când se execută atribuirea mes = Message{"hello"};, un nou obiect Message cu textul "hello" este creat (cu numărul 2).
3) În cadrul operatorului de atribuire cu mutare, se creează un obiect temporar și se mută datele din obiectul mes în acest obiect temporar.
4) Apoi se face schimbul între obiectul curent și obiectul temporar. După schimb, obiectul temporar este distrus, iar memoria este eliberată.
Astfel, idiomul move-and-swap ajută la crearea operatorilor de atribuire eficienți și siguri, minimizând riscurile de scurgeri de memorie și alte erori asociate cu gestionarea resurselor.