Desafio C++

Implemente a regra dos 5 completa

Enunciado

Implemente a regra dos 5 completa para a classe String: construtor comum, construtor de cópia, atribuição de cópia, construtor de movimento, atribuição de movimento e destrutor. Teste a auto-atribuição e a movimentação. O programa deve imprimir mensagens indicando qual operação foi chamada.

Requisitos

  • Todos os cinco membros especiais são definidos.
  • A atribuição de cópia e movimento tratam auto-atribuição (this == &other).
  • O destrutor libera memória e lida com nullptr.
  • A saída mostra a sequência de operações.

Código inicial

#include <iostream>
#include <cstring>
#include <utility>

class String {
    char* data_;
public:
    String(const char* s) {
        data_ = new char[std::strlen(s) + 1];
        std::strcpy(data_, s);
        std::cout << "Construtor\n";
    }
    // TODO: construtor de cópia, atribuição de cópia,
    // construtor de movimento, atribuição de movimento, destrutor
    const char* c_str() const { return data_ ? data_ : ""; }
};

int main() {
    String a("alpha");
    String b = a;            // cópia
    String c = std::move(a); // movimento
    b = c;                   // atribuição de cópia
    c = std::move(b);        // atribuição de movimento
    b = b;                   // auto-atribuição
    std::cout << "c: " << c.c_str() << "\n";
    return 0;
}

Saída esperada

Construtor
Cópia
Movimento
Atribuição de cópia
Atribuição de movimento
Auto-atribuição
c: alpha
Ver dica

Na atribuição, verifique if (this != &other) antes de liberar recursos. Use std::exchange para simplificar.

Mostrar solução
#include <iostream>
#include <cstring>
#include <utility>

class String {
    char* data_;
public:
    // Construtor comum
    String(const char* s) {
        data_ = new char[std::strlen(s) + 1];
        std::strcpy(data_, s);
        std::cout << "Construtor\n";
    }

    // Construtor de cópia
    String(const String& other) {
        data_ = new char[std::strlen(other.data_) + 1];
        std::strcpy(data_, other.data_);
        std::cout << "Cópia\n";
    }

    // Atribuição de cópia
    String& operator=(const String& other) {
        if (this != &other) {
            delete[] data_;
            data_ = new char[std::strlen(other.data_) + 1];
            std::strcpy(data_, other.data_);
        }
        std::cout << "Atribuição de cópia\n";
        return *this;
    }

    // Construtor de movimento
    String(String&& other) noexcept : data_(other.data_) {
        other.data_ = nullptr;
        std::cout << "Movimento\n";
    }

    // Atribuição de movimento
    String& operator=(String&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            other.data_ = nullptr;
        }
        std::cout << "Atribuição de movimento\n";
        return *this;
    }

    // Destrutor
    ~String() { delete[] data_; }

    const char* c_str() const { return data_ ? data_ : ""; }
};

int main() {
    String a("alpha");
    String b = a;            // cópia
    String c = std::move(a); // movimento
    b = c;                   // atribuição de cópia
    c = std::move(b);        // atribuição de movimento
    b = b;                   // auto-atribuição
    std::cout << "c: " << c.c_str() << "\n";
    return 0;
}

Passo a passo

  1. O construtor comum aloca e copia a string, imprimindo "Construtor".
  2. O construtor de cópia faz cópia profunda e imprime "Cópia".
  3. A atribuição de cópia verifica auto-atribuição, libera o buffer antigo, aloca novo e copia. Imprime "Atribuição de cópia".
  4. O construtor de movimento transfere o ponteiro e zera o original, imprimindo "Movimento".
  5. A atribuição de movimento verifica auto-atribuição, libera o buffer antigo, transfere o ponteiro e zera o original. Imprime "Atribuição de movimento".
  6. O destrutor libera a memória.
  7. No main, as operações são executadas na ordem, e a auto-atribuição é tratada sem liberar memória prematuramente.

Por que funciona

A regra dos 5 garante que todos os recursos sejam gerenciados corretamente. A verificação de auto-atribuição evita que delete[] data_ libere a memória antes de copiá-la. O movimento transfere a posse sem alocar, melhorando o desempenho.

Erros comuns

  • Esquecer a auto-atribuição: b = b; libera data_ e depois tenta copiar de si mesmo, causando use-after-free.
  • Não liberar memória na atribuição: vazamento de memória.
  • Não zerar o original no movimento: double free.
  • Não marcar movimento como noexcept: std::vector pode não usar movimento.

Outra forma de resolver

Usar a copy-and-swap idiom para simplificar as atribuições:

String& operator=(String other) {
    std::swap(data_, other.data_);
    return *this;
}

Saída esperada

Construtor
Cópia
Movimento
Atribuição de cópia
Atribuição de movimento
Auto-atribuição
c: alpha