Sobes.tech
Middle+

O que é semântica de movimentação e como usá-la?

sobes.tech IA

Resposta da IA

A semântica de movimento (move semantics) em C++11 e versões superiores permite transferir de forma eficiente a propriedade de um recurso de um objeto para outro, evitando cópias desnecessárias. Isto é conseguido através do uso de referências rvalue (&&) e do operador de movimento (std::move).

Aspectos-chave:

  • Referências rvalue (&&): Referências a objetos temporários (ou seja, rvalues) ou a objetos marcados como movíveis. Permitem ligar-se a um objeto que será destruído em breve.
  • Operador de movimento (std::move): Converte um lvalue numa referência rvalue. Por si só, não realiza o movimento, mas indica ao compilador que o objeto pode ser movido com segurança.
  • Construtor de movimento: Construtor específico que aceita uma referência rvalue como argumento. "Rouba" recursos (por exemplo, ponteiros para memória dinâmica) do objeto original, deixando-o num estado válido mas esvaziado.
  • Operador de atribuição por movimento: Semelhante ao construtor de movimento, destinado a operações de atribuição.

Aplicações:

  1. Otimização: Reduz os custos associados à cópia de objetos grandes ou que possuem recursos (por exemplo, contêineres, cadeias, ponteiros inteligentes).
  2. Implementação de contêineres eficientes: Permite a contêineres como std::vector gerenciar eficazmente os elementos ao alterar o tamanho ou mover-los.
  3. Devolver objetos grandes por valor: O compilador pode aplicar automaticamente a otimização de movimento (return value optimization, RVO, ou Named Return Values Optimization, NRVO), se o objeto devolvido for uma variável local ou um objeto temporário.

Exemplo:

#include <iostream>
#include <vector>
#include <string>
#include <utility> // Para std::move

class MyResource {
public:
    std::vector<int> data;

    // Construtor
    MyResource(size_t size) : data(size) {
        std::cout << "Construtor" << std::endl;
    }

    // Destrutor
    ~MyResource() {
        std::cout << "Destrutor" << std::endl;
    }

    // Construtor de cópia
    MyResource(const MyResource& other) : data(other.data) {
        std::cout << "Construtor de cópia" << std::endl;
    }

    // Operador de atribuição de cópia
    MyResource& operator=(const MyResource& other) {
        if (this != &other) {
            data = other.data;
        }
        std::cout << "Operador de atribuição de cópia" << std::endl;
        return *this;
    }

    // Construtor de movimento
    MyResource(MyResource&& other) noexcept : data(std::move(other.data)) {
        std::cout << "Construtor de movimento" << std::endl;
        // Após o movimento, o objeto original está num estado válido mas esvaziado
        // other.data permanece válido, mas seu conteúdo pode ter sido movido
    }

    // Operador de atribuição por movimento
    MyResource& operator=(MyResource&& other) noexcept {
        if (this != &other) {
            data = std::move(other.data);
        }
        std::cout << "Operador de atribuição por movimento" << std::endl;
        return *this;
    }
};

int main() {
    MyResource res1(10); // Construtor
    std::cout << "---" << std::endl;
    MyResource res2 = res1; // Construtor de cópia (ou operador de atribuição de cópia, se res2 já existir)
    std::cout << "---" << std::endl;
    MyResource res3 = std::move(res1); // Construtor de movimento
    std::cout << "---" << std::endl;
    MyResource res4(5); // Construtor
    res4 = std::move(res3); // Operador de atribuição por movimento
    std::cout << "---" << std::endl;

    return 0;
}

Neste exemplo, é possível ver que ao usar std::move, são chamados o construtor de movimento ou o operador de atribuição por movimento em vez de seus equivalentes de cópia, o que evita cópias dispendiosas do conteúdo do vetor data.