Semântica de Movimento e Rvalues
Parte da seção Programação Orientada a Objetos do Journey de C++ da Coddy — lição 83 de 104.
Em C++, toda expressão é um lvalue (possui uma identidade persistente, pode ser endereçada) ou um rvalue (temporário, prestes a ser destruído). Compreender essa distinção desbloqueia a move semantics - uma otimização poderosa que evita cópias desnecessárias.
Uma referência rvalue, declarada com &&, vincula-se especificamente a objetos temporários. Isso permite que você "roube" recursos de objetos que estão prestes a desaparecer de qualquer maneira:
#include <iostream>
#include <utility>
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : data(new int[s]), size(s) {
std::cout << "Constructed\n";
}
// Construtor de movimento - rouba recursos
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Deixa a origem em um estado válido
other.size = 0;
std::cout << "Moved\n";
}
~Buffer() { delete[] data; }
};
int main() {
Buffer b1(1000);
Buffer b2(std::move(b1)); // Aciona o construtor de movimento
}A função std::move não move nada de fato - ela simplesmente converte um lvalue para uma referência rvalue, sinalizando que você está disposto a abrir mão dos recursos do objeto. A movimentação real ocorre no construtor de movimento ou no operador de atribuição por movimento.
A semântica de movimento melhora drasticamente o desempenho ao trabalhar com objetos pesados em recursos, como contêineres ou strings. Em vez de realizar a cópia profunda de megabytes de dados, você simplesmente transfere a propriedade do ponteiro — uma operação de tempo constante, independentemente do tamanho.
Desafio
FácilVamos construir uma classe DataBuffer gerenciadora de recursos que demonstra a semântica de movimento em ação. Você verá como mover recursos em vez de copiá-los pode melhorar drasticamente a eficiência ao transferir a propriedade de memória alocada dinamicamente.
Você organizará seu código em três arquivos:
DataBuffer.h: Defina sua classeDataBufferque gerencia um array de inteiros alocado dinamicamente.Sua classe deve ter membros privados para o ponteiro de dados (
int*), o tamanho (size_t) e um nome (std::string) para ajudar a rastrear qual buffer é qual durante as operações.Declare o seguinte:
- Um construtor que recebe um nome
std::stringe um tamanhosize_t, aloca o array e imprime:[name] constructed with size [size] - Um construtor de movimento que recebe uma referência rvalue, rouba os recursos e imprime:
[name] moved from [source_name](onde o buffer de destino assume o nome da origem) - Um destruidor que imprime
[name] destroyed(ouempty destroyedse o buffer foi movido) - Um método
getSize()que retorna o tamanho atual - Um método
getName()que retorna o nome do buffer
Lembre-se de marcar seu construtor de movimento como
noexcepte deixar o objeto de origem em um estado vazio válido (nullptr, tamanho 0, nome "empty").- Um construtor que recebe um nome
DataBuffer.cpp: Implemente todos os métodos declarados em seu cabeçalho. Quando o destruidor for executado, delete os dados apenas se o ponteiro não for nulo. Inclua<iostream>para saída.main.cpp: Leia duas entradas:- Um nome para o seu buffer (string)
- Um tamanho para o seu buffer (inteiro)
Crie um
DataBuffercom o nome e tamanho fornecidos. Em seguida, crie um segundo buffer movendo do primeiro usandostd::move(). Após o movimento, imprima o estado de ambos os buffers:Original: [name] size=[size]New: [name] size=[size]
Inclua
<utility>parastd::move.
Por exemplo, com as entradas Alpha e 100:
Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyedCom as entradas Buffer e 50:
Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyedObserve como o construtor de movimento transfere a propriedade da memória alocada sem copiar nenhum dado. O buffer original é deixado em um estado vazio, mas válido, e quando ambos os buffers são destruídos ao final do programa, apenas aquele que ainda possui a memória realmente a deleta.
Folha de consulta
Expressões C++ são **lvalues** (persistentes, endereçáveis) ou **rvalues** (temporárias). A **semântica de movimento** otimiza o desempenho transferindo recursos de objetos temporários em vez de copiá-los.
Referências Rvalue
Declaradas com &&, referências rvalue se vinculam a objetos temporários:
Buffer(Buffer&& other) // Parâmetro de referência rvalueConstrutor de Movimento
Rouba recursos de um objeto temporário. Marque-o como noexcept e deixe a origem em um estado vazio válido:
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Deixa a origem válida
other.size = 0;
}std::move
Converte um lvalue para uma referência rvalue, sinalizando a disposição para transferir recursos. Inclua <utility>:
Buffer b1(1000);
Buffer b2(std::move(b1)); // Aciona o construtor de movimentostd::move não move nada por si só - a transferência real acontece no construtor de movimento ou no operador de atribuição por movimento.
Benefícios
A semântica de movimento fornece transferência de recursos em tempo constante, independentemente do tamanho do objeto, evitando cópias profundas dispendiosas de grandes estruturas de dados.
Experimente você mesmo
#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"
int main() {
std::string name;
int size;
std::cin >> name;
std::cin >> size;
// TODO: Criar um DataBuffer com o nome e tamanho fornecidos
// TODO: Criar um segundo buffer movendo do primeiro usando std::move()
// TODO: Imprimir o estado de ambos os buffers:
// Original: [name] size=[size]
// Novo: [name] size=[size]
return 0;
}
Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.
Todas as lições de Programação Orientada a Objetos
1Fundamentos de OOP
Arquivos ExternosBuild e Compilação em C++Arquivos Header e SourceNamespaces e EscopoIntrodução a OOP em C++Classes vs ObjetosO Ponteiro 'this'Métodos (Funções Membro)Atributos (Membros de Dados)Básico de Ctors e DtorsRecapitulação - Calculadora Simples4Propriedades de Classe
Membros de Instância vs EstáticosGetters e SettersFunções de Membro ConstPalavra-chave MutableMétodos e Variáveis EstáticosFunções e Classes FriendRecapitulação - Gerenciador de Conta Bancária7Herança
Herança BásicaNíveis de Acesso na HerançaOrdem de Chamada de Ctor e DtorSobrescrita de MétodosFunções Virtuais e VTableHerança MúltiplaHerança VirtualRecapitulação - Hierarquia de Funcionários2Gerenciamento de Memória
Memória Stack vs HeapPonteiros e ReferênciasMemória Dinâmica (new/delete)Smart Pointers em C++RAII em C++Recapitulação - Gerenciador de Array Dinâmico5Encapsulamento
Especificadores de Acesso em C++Especificadores de Acesso em DetalhesOcultação de InformaçãoStruct vs ClassClasses Aninhadas e InternasRecapitulação - Sistema de Registros de Alunos8Polimorfismo
Polimorfismo de Compilação vs Tempo de ExecuçãoSobrecarga de FunçõesFunções Virtuais RevisitadasFunções Virtuais PurasClasses AbstratasDesign de Interfaces em C++Dynamic Casting e RTTIRecapitulação - Calculadora de Formas3Construtores e Destrutores
Construtor PadrãoConstrutor ParametrizadoConstrutor de CópiaConstrutor de MovimentaçãoListas de Inicialização do ConstrutorConstrutores DelegadosMergulho Profundo em DestrutoresRegra dos Três / Cinco / ZeroRecapitulação - Classe String6Sobrecarga de Operadores
Introdução à Sobrecarga de OperadoresSobrecarga de Operadores AritméticosSobrecarga de Operadores de ComparaçãoOperadores de StreamSobrecarga do Operador de AtribuiçãoSobrecarga dos Operadores [] e ()Operadores de Conversão de TipoRecapitulação - Classe Matrix9Templates
Templates de FunçãoTemplates de ClasseEspecialização de TemplatesTemplates VariádicosFundamentos de SFINAE e Type TraitsRecapitulação - Container Genérico12Recursos do C++ Moderno
Semântica de Movimento e RvaluesPerfect ForwardingExpressões Lambda em Detalhesstd::function & std::bindconstexpr e constevalStructured Bindingsoptional, variant, any