Constructorul de mutare
Constructorul de mutare (move constructor) reprezintă o alternativă la constructorul de copiere în situațiile în care trebuie făcută o copie a unui obiect, dar copierea datelor nu este dorită — în loc să se copieze datele, acestea sunt pur și simplu mutate dintr-o copie în alta.
Să analizăm un exemplu.
#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;
}
// constructor de copiere
Message(const Message& copy) : Message{copy.getText(), copy.size } // apelăm constructorul standard
{
std::cout << "Copy Message " << copy.id << " to " << id << std::endl;
}
// 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 id-ului obiectului
};
// clasa messenger, care trimite mesajul
class Messenger
{
public:
Messenger(Message mes): message{mes}
{ }
void sendMessage() const
{
std::cout << "Send message " << message.getId() << ": " << message.getText() << std::endl;
}
private:
Message message;
};
int main()
{
Messenger telegram{Message{"Hello Word", 11}};
telegram.sendMessage();
}
Aici este definită clasa Message. Textul mesajului este stocat într-un pointer de caractere text. Pentru a observa procesul complet de creare/copiere/ștergere a obiectelor Message, clasa conține o variabilă statică counter care crește la crearea fiecărui nou obiect și este atribuită variabilei id, reprezentând numărul mesajului:
char* text{}; // textul mesajului
unsigned size{}; // dimensiunea mesajului
unsigned id{}; // numărul mesajului
static inline unsigned counter{};
Constructorul Message alocă memorie pentru textul mesajului primit prin parametrul data, copiază datele în memorie și stabilește numărul mesajului. Pentru copierea datelor este definit constructorul de copiere.
De asemenea este definită clasa Messenger, care în constructor primește un mesaj și îl salvează în variabila message:
class Messenger
{
public:
Messenger(Message mes): message{mes}
{ }
Prin funcția sendMessage messengerul trimite efectiv mesajul.
În funcția main creăm un obiect Messenger, căruia îi transmitem un obiect mesaj, apoi apelăm funcția sendMessage:
Messenger telegram{Message{"Hello Word", 11}};
telegram.sendMessage();
Observați că la constructorul obiectului Messenger este transmis un obiect Message care nu este legat de nicio variabilă. Să analizăm output-ul în consolă:
Create message 1
Create message 2
Copy message 1 to 2
Delete message 1
Send message 2: Hello Word
Delete message 2
Vedem că în timpul execuției programului se creează două obiecte Message, fiind implicat constructorul de copiere. Să vedem pas cu pas.
Executarea liniei:
Messenger telegram{Message{"Hello Word", 11}};
duce la execuția constructorului Message, în care șirul "Hello Word" este transmis variabilei text și se stabilește numărul mesajului. Acest obiect temporar Message primește numărul 1. Se afișează:
Create message 1
Obiectul Message creat este transmis constructorului Messenger:
Messenger(Message mes): message{mes}
Observați expresia message{mes}. Aceasta ia obiectul temporar Message și prin constructorul de copiere creează o copie care este atribuită variabilei message. Constructorul de copiere apelează la rândul său constructorul standard:
Message(const Message& copy) : Message{copy.getText(), copy.size }
Se creează obiectul Message cu numărul 2. Constructorul standard alocă memorie pentru șir. Avem două copii independente, fiecare cu pointeri la locații diferite în memorie. Astfel, în consolă se afișează:
Create message 2
Copy message 1 to 2
Delete message 1
Obiectul Messenger deține acum al doilea mesaj, iar primul, temporar, este șters.
Apoi se apelează funcția sendMessage:
telegram.sendMessage();
Mesajul stocat în messenger este afișat în consolă, iar la terminarea funcției main mesajul este șters:
Send message 2: Hello Word
Delete message 2
Din punct de vedere al copierii, al alocării/gestionării/eliberării memoriei, nu par să existe probleme. Dar observăm că memoria alocată pentru primul mesaj nu a fost folosită efectiv. Cu alte cuvinte, am folosit inutil acea memorie. Nu ar fi mai bine dacă, în loc să alocăm o nouă zonă de memorie pentru al doilea mesaj, am putea pur și simplu să îi transmitem memoria deja alocată pentru primul mesaj? Oricum, primul mesaj este șters. Iar pentru a rezolva această problemă se folosesc constructorii de mutare.
Constructorul de mutare primește un singur parametru, care trebuie să fie o referință rvalue la un obiect de tipul curent:
MyClass(MyClass&& moved) // referință rvalue
{
// codul constructorului de mutare
}
Aici parametrul moved reprezintă obiectul ce urmează să fie mutat.
Să modificăm codul de mai sus aplicând constructorul de mutare în clasa Message:
#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;
}
// constructor de copiere
Message(const Message& copy) : Message{copy.getText(), copy.size } // apelăm constructorul standard
{
std::cout << "Copy Message " << copy.id << " to " << id << std::endl;
}
// constructor de mutare
Message(Message&& moved)
{
id = ++counter;
std::cout << "Create Message " << id << std::endl;
text = moved.text; // mutăm textul mesajului
size = moved.size; // mutăm dimensiunea mesajului
moved.text = nullptr;
std::cout << "Move Message " << moved.id << " to " << id << std::endl;
}
// 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
};
// clasa messenger care trimite mesajul
class Messenger
{
public:
Messenger(Message mes): message{std::move(mes)}
{ }
void sendMessage() const
{
std::cout << "Send message " << message.getId() << ": " << message.getText() << std::endl;
}
private:
Message message;
};
int main()
{
Messenger telegram{Message{"Hello Word", 11}};
telegram.sendMessage();
}
Comparativ cu codul anterior, aici sunt două modificări. În primul rând, în clasa Message a fost adăugat constructorul de mutare:
Parametrul moved reprezintă obiectul care se mută. Nu apelăm constructorul standard, ca la constructorul de copiere, deoarece nu trebuie să alocăm memorie. În schimb, pur și simplu atribuim variabilei text valoarea pointerului (adresa blocului de memorie alocat) din obiectul moved care se mută.
text = moved.text
Astfel evităm alocarea suplimentară inutilă de memorie. Și pentru ca pointerul text al obiectului mutat moved să nu mai indice către acea zonă de memorie, iar destructorul obiectului moved să nu elibereze acea memorie, îi atribuim pointerului valoarea nullptr.
Un alt aspect important — în constructorul clasei Messenger, la copierea obiectului folosim funcția încorporată std::move(), disponibilă în biblioteca standard C++:
class Messenger
{
public:
Messenger(Message mes): message{std::move(mes)}
{ }
Funcția std::move() transformă valoarea transmisă într-o referință rvalue. În ciuda numelui, această funcție nu mută nimic.
Expresia message{std::move(mes)} va duce practic la apelul constructorului de mutare, căruia i se transmite parametrul mes. Rezultatul constructorului de mutare este apoi atribuit variabilei message. Astfel, în consolă vom obține următorul output:
Create message 1
Create Message 2
Move message 1 to 2
Delete message 1
Send message 2: Hello Word
Delete message 2
Dacă constructorul de mutare nu ar fi fost definit, expresia message{std::move(mes)} ar fi apelat constructorul de copiere.
Așadar, se creează tot două obiecte, dar evităm alocarea inutilă de memorie și copierea datelor. În general, se mută obiectele care nu mai sunt necesare, ca în exemplul de mai sus.
Trebuie remarcat că compilatorul generează automat un constructor de mutare implicit, care mută toate variabilele nestatice. Dar dacă definim manual destructorul, constructorul de copiere sau operatorul de atribuire prin copiere ori mutare, compilatorul nu mai generează constructorul de mutare implicit.
Exemplu cu vectori
Un alt exemplu relevant de folosire a constructorului de mutare este adăugarea unui obiect în vector. Tipul std::vector reprezintă o listă dinamică și pentru adăugarea unui obiect definește funcția push_back(), care are două versiuni:
void push_back(const Message &_Val)
void push_back(Message &&_Val)
Prima versiune primește o referință constantă și este destinată în primul rând pentru a primi lvalue. A doua versiune primește un rvalue.
Să luăm clasa Message definită anterior și să adăugăm un obiect în vector:
#include <iostream>
#include <vector>
// clasa Message
class Message
{
// conținutul clasei Message
};
int main()
{
std::vector<Message> messages{};
messages.push_back(Message{"Hello world", 12});
}
Aici în vector este adăugat un obiect Message ca rvalue. În implementarea internă, obiectul adăugat va fi salvat, iar la salvare se va apela constructorul de mutare pentru a muta datele din rvalue. Output-ul în consolă:
Create Message 1
Create Message 2
Move Message 1 to 2
Delete Message 1
Delete Message 2
Astfel evităm din nou costurile suplimentare ale copieri inutile și mutăm datele în loc să le copiem.
Dacă însă transmitem în funcția push_back() un lvalue, va fi apelată versiunea cu referință constantă, iar în final constructorul de copiere va crea o copie:
int main()
{
Message mes{"Hello world", 12};
std::vector<Message> messages{};
messages.push_back(mes);
}
Output-ul în consolă:
Create message 1
Create message 2
Copy message 1 to 2
Delete message 2
Delete message 1