Cópia, movimento e propriedade

Cópia superficial e profunda, atribuição, autoatribuição, movimento e regras de três, cinco e zero.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Copiar o relógio duplica o seu inteiro. As duas horas evoluem depois separadamente.

Se o membro fosse um apontador dono, a cópia gerada duplicaria apenas o endereço. A classe precisa de decidir se uma cópia terá outro recurso, partilhará o original ou será proibida.

Construção de cópia e atribuição

Em T b = a;, b ainda não existe e é construído a partir de a. Em b = a;, b já existe e recebe outro estado. A construção de cópia usa habitualmente T(const T&); a atribuição de cópia usa T& operator=(const T&).

A versão gerada pelo compilador copia bases e membros. Se o membro é um int, copia o valor. Se é um std::vector, o vetor trata a cópia dos seus elementos. Se é um apontador cru, copia o endereço.

Cópia superficial e profunda

Considera este excerto incorreto como classe dona de memória. Não o executes.

class DonoIncompleto {
public:
    DonoIncompleto() : dados_(new int(5)) {}
    ~DonoIncompleto() { delete dados_; }
private:
    int* dados_;
};

A cópia implícita de um DonoIncompleto deixa os dois objetos com o mesmo endereço. Os dois destrutores tentam depois libertar o mesmo inteiro; a propriedade fica duplicada sem uma regra válida de destruição.

Uma cópia superficial copia a representação, incluindo endereços. Uma cópia profunda cria recursos independentes e copia o seu conteúdo. Partilhar um recurso também pode ser correto, mas precisa de uma política explícita, como a de shared_ptr.

Para um buffer com os valores 4 e 6, a cópia profunda cria outro array, por isso depois de b.at(0) = 9 cada objeto tem o seu recurso:

Construção de uma cópia independente

O exemplo usa unique_ptr<int[]> para garantir a libertação, mas implementa a cópia para dar ao Buffer semântica de valor.

O invariante é que existem tamanho_ inteiros acessíveis; quando o tamanho é zero, não existe elemento válido. at verifica o índice.

Começa pela construção de cópia; a atribuição e o movimento vêm a seguir:

// Excerto: a cópia reserva e copia, nunca partilha o endereço.
Buffer(const Buffer& outro) : Buffer(outro.tamanho_) {
    for (std::size_t i = 0; i < tamanho_; ++i) dados_[i] = outro.dados_[i];
}

Com Buffer a(2) contendo 4 e 6, Buffer b = a reserva dois inteiros novos e copia cada posição. Alterar b.at(0) para 9 não afeta a. A delegação Buffer(outro.tamanho_) constrói primeiro um destino válido; se a reserva falhar, a exceção sai antes de existir qualquer cópia parcial.

Um buffer com propriedade exclusiva

O programa completo seguinte reúne cópia, atribuição e movimento. Na atribuição, o destino já tem um recurso: no exemplo abaixo, c contém 2, 3 e 5 e recebe b com dois inteiros. A técnica é preparar primeiro a cópia completa num objeto temporário e só depois trocar com o destino, para que o destino continue intacto se a reserva falhar. A tabela depois da listagem mostra os três momentos: preparar, trocar e destruir o antigo.

Programa completo para compilar localmente:

#include <cstddef>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <utility>

class Buffer {
public:
    explicit Buffer(std::size_t n)
        : tamanho_(n), dados_(n == 0 ? nullptr : std::make_unique<int[]>(n)) {}

    Buffer(const Buffer& outro) : Buffer(outro.tamanho_) {
        for (std::size_t i = 0; i < tamanho_; ++i) dados_[i] = outro.dados_[i];
    }

    Buffer& operator=(const Buffer& outro) {
        if (this == &outro) return *this;
        Buffer copia(outro);
        trocar(copia);
        return *this;
    }

    Buffer(Buffer&& outro) noexcept
        : tamanho_(std::exchange(outro.tamanho_, 0)),
          dados_(std::move(outro.dados_)) {}

    Buffer& operator=(Buffer&& outro) noexcept {
        if (this == &outro) return *this;
        dados_ = std::move(outro.dados_);
        tamanho_ = std::exchange(outro.tamanho_, 0);
        return *this;
    }

    ~Buffer() = default;
    std::size_t size() const { return tamanho_; }
    int& at(std::size_t i) {
        if (i >= tamanho_) throw std::out_of_range("Indice invalido");
        return dados_[i];
    }
    const int& at(std::size_t i) const {
        if (i >= tamanho_) throw std::out_of_range("Indice invalido");
        return dados_[i];
    }
private:
    void trocar(Buffer& outro) noexcept {
        std::swap(tamanho_, outro.tamanho_);
        dados_.swap(outro.dados_);
    }
    std::size_t tamanho_;
    std::unique_ptr<int[]> dados_;
};

int main() {
    Buffer a(2);
    a.at(0) = 4;
    a.at(1) = 6;
    Buffer b = a;
    b.at(0) = 9;
    Buffer c(3);
    c.at(0) = 2;
    c.at(1) = 3;
    c.at(2) = 5;
    c = b;
    const Buffer& alias = c;
    c = alias;
    Buffer d = std::move(b);
    std::cout << a.at(0) << " " << c.at(0) << " " << d.at(0) << "\n";
    std::cout << b.size() << " " << d.size() << "\n";
}

Saída:

4 9 9
0 2

std::make_unique<int[]>(n) reserva um array de n inteiros inicializados a zero. A expressão n == 0 ? nullptr : ... escolhe não reservar quando o tamanho é zero. std::exchange(outro.tamanho_, 0) devolve o tamanho antigo e coloca zero na origem.

Buffer b = a reserva dois inteiros novos e copia 4 e 6, por isso alterar b.at(0) para 9 não afeta a. c = b substitui os três inteiros de um destino que já existia pelos dois inteiros 9 e 6.

MomentoRecurso de cRecurso de copia
Antes da atribuição2, 3, 5ainda não existe
Preparar copia(b)2, 3, 5, intactonovo array com 9, 6
trocar(copia)9, 6antigo array com 2, 3, 5
Fim da atribuição9, 6destruído com o antigo array

A autoatribuição através de alias mantém c intacto. Buffer d = std::move(b) transfere o endereço sem copiar inteiros; b fica com tamanho zero.

Atribuir sem perder o recurso anterior

A técnica copy-and-swap prepara o novo recurso antes de perder o anterior e dá garantia forte: a atribuição constrói primeiro copia, por isso se a reserva falhar a exceção sai antes de tocares no destino; depois, trocar troca tamanho e propriedade sem lançar.

A alternativa “libertar o destino e só depois copiar” perde o estado anterior se a reserva falhar. Também falha na autoatribuição se destrói o recurso que ainda pretende ler. Testar this == &outro resolve a autoatribuição, mas não resolve sozinho a segurança perante falhas de reserva.

operator= devolve *this por referência, permitindo expressões como a = b = c. A atribuição não deve devolver uma referência ao temporário de cópia.

Movimento não é uma cópia profunda

std::move permite tratar uma expressão como candidata a movimento. Não transfere nada sozinho. A operação selecionada, aqui o construtor Buffer(Buffer&&), é que transfere a propriedade.

O movimento de b para d transfere o endereço sem copiar os inteiros e põe o tamanho de b a zero. Por isso, b fica vazio e pode ser destruído ou receber nova atribuição.

O estado de um objeto movido depende do contrato do tipo. Não assumes que qualquer std::string ou std::vector movido fica vazio. A biblioteca garante um estado válido, mas não especificado, salvo garantias adicionais do tipo.

Podes destruí-lo, atribuir-lhe outro valor e usar operações cujas pré-condições satisfaças. Por exemplo, v.empty() e v.size() são consultas válidas depois de mover um vetor. v.front() exige confirmar que não está vazio.

Num movimento entre dois objetos distintos, o Buffer desta página deixa a origem com tamanho zero. A autoatribuição por movimento é detetada por this == &outro e conserva o objeto. noexcept indica que as operações de movimento apresentadas não lançam, o que também ajuda coleções a escolher movimento durante uma realocação.

Regras de três, cinco e zero

A regra de três manda rever destrutor, construtor de cópia e atribuição de cópia quando a gestão manual de um recurso exige uma destas operações. Em C++ com movimento, a regra de cinco acrescenta construtor e atribuição de movimento. Não significa que todos os tipos devam implementar cinco funções; uma operação pode ser gerada, definida ou proibida com = delete, consoante o contrato.

A regra de zero é preferir membros que já gerem corretamente os recursos. A classe seguinte não precisa de nenhuma operação especial escrita à mão.

Regra de zero com vector
#include <cstddef>
#include <iostream>
#include <vector>

class Leituras {
public:
    void adicionar(int valor) { valores_.push_back(valor); }
    int primeiro() const { return valores_.at(0); }
    std::size_t tamanho() const { return valores_.size(); }
private:
    std::vector<int> valores_;
};

int main() {
    Leituras a;
    a.adicionar(4);
    Leituras b = a;
    b.adicionar(8);
    std::cout << a.primeiro() << " " << b.primeiro() << "\n";
    std::cout << a.tamanho() << " " << b.tamanho() << "\n";
}
Resultado

Executa o código para ver a saída e os gráficos.

Saída:

4 4
1 2

Cada Leituras possui o seu vetor. Acrescentar 8 à cópia deixa o original com um elemento e a cópia com dois. O compilador compõe as operações corretas a partir das do membro. O exemplo manual anterior ensina a mecânica; uma coleção habitual de inteiros deve usar vector para evitar reproduzir essa gestão.

Escolher a semântica

Se um tipo representa um valor, a cópia deve ser independente. Se representa propriedade exclusiva, proíbe a cópia e permite movimento. Decide isto antes de escreveres o destrutor, porque a política de cópia faz parte da interface.

Exercícios

Independência entre cópias

Uma classe possui um array dinâmico de dois inteiros. A cópia gerada pelo compilador copia só o apontador, e o destrutor usa delete[]. Explica o que corre mal e duas políticas válidas para o tipo.

Pista

Segue quem julga ser dono do mesmo array depois da cópia.

Solução

Uma alteração através de um objeto aparece no recurso do outro. Na destruição, ambos tentam libertar o mesmo array, causando comportamento indefinido. Uma política válida é implementar cópia profunda com recursos independentes. Outra é proibir cópia e permitir transferência por movimento. Se o significado exige partilha, usa uma propriedade partilhada explícita.

Confere a tua resposta:

  • Identifiquei o recurso único com dois donos indevidos.
  • Expliquei aliasing e dupla libertação.
  • Propus duas políticas coerentes com o contrato.

Erros frequentes

Acrescentar apenas um destrutor não completa a semântica de uma classe dona.

Voltar à explicação
Operações válidas depois do movimento

No Buffer da lição, executa-se Buffer a(3); Buffer b = std::move(a);. Indica os tamanhos finais e se a.at(0) pode devolver um inteiro. Noutro programa, move-se um std::vector<int> v. É válido consultar v.size()? E chamar v.front() sem verificar nada? Justifica usando o contrato de cada tipo.

Pista

Um objeto válido não garante a existência de um primeiro elemento.

Solução

O construtor de movimento do Buffer põe o tamanho de a a zero e transfere os três elementos para b. a.at(0) lança out_of_range porque não existe índice válido. No vetor movido, o estado é válido mas não se promete que fique vazio. size() é uma consulta válida; antes de front(), confirma !v.empty(). std::move sozinho não impõe estas garantias.

Confere a tua resposta:

  • Indiquei a.size() igual a zero e b.size() igual a três no Buffer.
  • Distingui consultar o tamanho de aceder a um elemento.
  • Não pressupus que um vector movido fica vazio.
Voltar à explicação
Atribuir sem perder o recurso anterior

Um Buffer c(3) contém 2, 3, 5 e recebe c = b, onde b contém 9, 6. Descreve os três momentos: preparar a cópia, trocar e destruir o antigo. Explica o que aconteceria se a reserva da cópia falhasse em cada alternativa.

Pista

Segue quem é dono de cada array em cada momento.

Solução

Primeiro constrói-se copia(b) com recurso próprio. Depois a troca entrega esse recurso a c. No fim, o recurso antigo de c morre com copia. Se a reserva falhar na preparação, c continua com os três valores 2, 3, 5. Na alternativa de libertar primeiro, a falha deixaria c sem recurso e sem valor anterior.

Confere a tua resposta:

  • Preparei a cópia antes de tocar no destino.
  • Expliquei quando o recurso antigo é libertado.
  • Mostrei o que corre mal ao libertar primeiro e copiar depois.

Erros frequentes

Testar autoatribuição não chega. A ordem prepara-depois-troca é que dá a garantia forte.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.