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

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 (&copy != 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.