MySQL Java JavaScript PHP Python HTML-CSS C-sharp C++ Go

Rolul noexcept la mutare

Atunci când definim constructori de mutare și operatori de atribuire prin mutare, este recomandat să îi declarăm cu specificatorul noexcept, dacă aceste funcții în principiu nu pot arunca excepții. Să vedem mai întâi de ce este important acest lucru.

Tipul std::vector reprezintă o listă dinamică și pentru adăugarea unui obiect oferă funcția push_back(). Această funcție are două versiuni:

void push_back(const Message &_Val)
void push_back(Message &&_Val)

Adică dacă transmitem un rvalue se apelează a doua versiune, care folosește constructorul de mutare pentru a păstra datele în vector. Să vedem ce se întâmplă dacă adăugăm mai multe obiecte în vector:

#include <iostream>
#include <vector>
  
// 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;  // copiem 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
};
 
int main()
{
    std::vector<Message> messages{};
    messages.push_back(Message{"Hello world", 12});
    messages.push_back(Message{"Bye world", 10});
}

Aici este definită clasa Message. Textul mesajului este stocat în memorie dinamică și accesibil prin pointerul text. Pentru claritate folosim pointeri normali, nu smart pointeri. De asemenea, pentru a observa procesul complet de creare/copiere/ștergere, în clasă este definită o variabilă statică counter, care crește la crearea fiecărui obiect nou, iar valoarea curentă este atribuită variabilei id, care reprezintă numărul mesajului:

char* text{};  // textul mesajului
unsigned size{};    // dimensiunea mesajului
unsigned id{};  // numărul mesajului
static inline unsigned counter{};   // contor static pentru generarea numărului obiectului

Constructorul Message alocă memorie dinamică pentru caracterele mesajului, setează dimensiunea și numărul mesajului, iar destructorul eliberează memoria. Pentru copiere este definit constructorul de copiere.

De asemenea, Message definește constructorul de mutare:

Message(Message&& moved)
{
    id = ++counter;
    std::cout << "Create Message " << id << std::endl;

    text = moved.text;  // mutăm textul mesajului
    size = moved.size;  // copiem dimensiunea mesajului
    moved.text = nullptr;
    std::cout << "Move Message " << moved.id << " to " << id << std::endl;
}

Aici nu apelăm constructorul standard ca la constructorul de copiere, pentru că nu trebuie să alocăm memorie. În schimb, atribuim pointerului text adresa zonei de memorie deja alocate din obiectul moved. Pentru a evita ca pointerul text din obiectul mutat să elibereze acea memorie la distrugere, îi atribuim nullptr.

În funcția main, în vector sunt adăugate două obiecte Message reprezentate de rvalue:

std::vector<Message> messages{};
messages.push_back(Message{"Hello world", 12});
messages.push_back(Message{"Bye world", 10});

Să vedem ce va afișa consola:

Create Message 1
Create Message 2
Move Message 1 to 2
Delete Message 1
Create Message 3
Create Message 4
Move Message 3 to 4
Create Message 5
Copy  Message 2 to 5
Delete Message 2
Delete Message 3
Delete Message 5
Delete Message 4

Deci, adăugăm în vector 2 obiecte Message, dar în total se creează 5 obiecte Message. Să analizăm pas cu pas.

Mai întâi adăugăm un obiect Message:

messages.push_back(Message{"Hello world", 12});

Se creează un obiect Message care reprezintă un rvalue. Datele din acesta sunt mutate în alt obiect Message care este stocat în vector, folosind constructorul de mutare.

Create Message 1
Create Message 2
Move Message 1 to 2
Delete Message 1

Vectorul este o listă dinamică care poate fi extinsă. Intern, la adăugarea unui nou element, vectorul alocă un nou bloc de memorie mai mare pentru a încăpea elementele existente și pe cele noi. Aceasta implică copierea datelor din vechiul bloc în cel nou. Astfel, la executarea liniei:

messages.push_back(Message{"Bye world", 10});

Se alocă memorie pentru două obiecte Message. Se creează un nou obiect rvalue, datele acestuia sunt mutate în obiectul din vector, dar în același timp primul obiect adăugat este copiat în noua zonă de memorie. La copiere este folosit constructorul de copiere, după cum arată consola:

Create Message 3
Create Message 4
Move Message 3 to 4
Create Message 5
Copy  Message 2 to 5
Delete Message 2
Delete Message 3

De ce se folosește constructorul de copiere și nu cel de mutare, care ar fi mai potrivit aici? Pentru că vectorul nu este sigur dacă constructorul de mutare nu va arunca o excepție, iar în acest caz preferă să folosească constructorul de copiere.

Dar în cazul nostru, constructorul de mutare nu aruncă excepții. Așadar, adăugăm cuvântul cheie noexcept la constructorul de mutare:

Message(Message&& moved) noexcept
{
    id = ++counter;
    std::cout << "Create Message " << id << std::endl;
 
    text = moved.text;  // mutăm textul mesajului
    size = moved.size;  // copiem dimensiunea mesajului
    moved.text = nullptr;   // resetăm pointerul obiectului mutat
    std::cout << "Move Message " << moved.id << " to " << id << std::endl;
}

Dacă recompilăm și rulăm programul, vom vedea că în locul constructorului de copiere se va folosi constructorul de mutare:

Create Message 1
Create Message 2
Move Message 1 to 2
Delete Message 1
Create Message 3
Create Message 4
Move Message 3 to 4
Create Message 5
Move Message 2 to 5
Delete Message 2
Delete Message 3
Delete Message 5
Delete Message 4

Acest mecanism de copiere este folosit nu doar de vector, deci constructorul de mutare și operatorul de atribuire prin mutare trebuie întotdeauna definite cu specificatorul noexcept.