Semântica de Movimentação 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 (tem uma identidade persistente, pode ter seu endereço obtido) ou um rvalue (temporária, prestes a ser destruída). Entender essa distinção permite compreender a semântica de movimentação — 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 “roubar” recursos de objetos que estão prestes a desaparecer de qualquer forma:
#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";
}
// Move constructor - steals resources
// Construtor de movimento - rouba recursos
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Leave source in valid state // 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)); // Triggers move constructor // Aciona o construtor de movimento
}A função std::move não move nada de fato — ela simplesmente converte um lvalue em uma referência rvalue, sinalizando que você está disposto a abrir mão dos recursos do objeto. A movimentação propriamente dita acontece 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 containers 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 criar uma classe DataBuffer que gerencia recursos e demonstra a semântica de movimentação na prática. 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 classeDataBuffer, que 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 identificar 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 movimentação que recebe uma referência rvalue, apropria-se dos recursos e imprime:
[name] moved from [source_name](em que o buffer de destino recebe o nome da origem) - Um destrutor que imprime
[name] destroyed(ouempty destroyedse o buffer tiver sido 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 movimentação 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 no cabeçalho. Quando o destrutor for executado, exclua os dados somente se o ponteiro não for nulo. Inclua<iostream>para a saída.main.cpp: leia duas entradas:- Um nome para seu buffer (string)
- Um tamanho para seu buffer (inteiro)
Crie um
DataBuffercom o nome e o tamanho fornecidos. Em seguida, crie um segundo buffer movendo o primeiro usandostd::move(). Depois da movimentação, 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 movimentação transfere a propriedade da memória alocada sem copiar nenhum dado. O buffer original permanece em um estado vazio, mas válido, e, quando ambos os buffers são destruídos no final do programa, somente aquele que ainda possui a memória a exclui de fato.
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 POO
Arquivos externosBuild e compilação em C++Arquivos de cabeçalho e arquivos-fonteNamespaces e escopoIntrodução à POO em C++Classes vs. objetosO ponteiro 'this'Métodos (funções membro)Atributos (membros de dados)Noções básicas de construtores e destruidoresRevisão - calculadora simples4Propriedades de Classe
Membros de Instância vs. EstáticosGetters e SettersFunções Membro ConstPalavra-chave MutableMétodos e Variáveis EstáticosFunções e Classes FriendRevisão - Gerenciador de Contas Bancárias7Herança
Herança básicaNíveis de acesso da herançaOrdem de chamada de construtor e destrutorSobrescrita de métodosFunções virtuais e VTableHerança múltiplaHerança virtualRevisão - Hierarquia de funcionários10Visão geral da STL
Visão geral e filosofia da STLContêineres da STLIteradoresAlgoritmos da STLFunctors e expressões lambdaRecapitulação - Frequência de palavras13Padrões de Projeto — Parte 1
Introdução aos Padrões de ProjetoPadrão SingletonFactory e Abstract FactoryPadrão BuilderPadrão ObserverPadrão Strategy2Gerenciamento de Memória
Memória Stack vs. HeapPonteiros e ReferênciasMemória Dinâmica (new/delete)Ponteiros Inteligentes em C++RAII em C++Revisão - Gerenciador de Array Dinâmico5Encapsulamento
Especificadores de acesso em C++Especificadores de acesso em profundidadeOcultação de informaçõesStruct vs ClasseClasses aninhadas e internasRecapitulação - Sistema de registros de alunos8Polimorfismo
Polimorfismo em compilação vs. execuçãoSobrecarga de funçõesFunções virtuais revisadasFunções virtuais purasClasses abstratasDesign de interfaces em C++Conversão dinâmica e RTTIRecapitulação — Calculadora de formas11Conceitos Avançados de OOP
Composição vs. HerançaMixins via CRTPIdioma PimplApagamento de TiposClasses Enum e Tipagem ForteTratamento de Exceções em OOPHierarquias de Exceções Personalizadas14Padrões de Projeto — Parte 2
Padrão CommandPadrão AdapterPadrão DecoratorPadrão Template MethodPadrão StatePadrão CompositeRAII como Padrão3Construtores e Destrutores
Construtor PadrãoConstrutor ParametrizadoConstrutor de CópiaConstrutor de MovimentoListas de Inicialização do ConstrutorConstrutores DelegantesAprofundamento em DestrutoresRegra dos Três / Cinco / ZeroRevisão - Classe String6Sobrecarga de Operadores
Introdução à Sobrecarga de OperadoresSobrecarga de Operadores AritméticosSobrecarga de Operadores de ComparaçãoOperadores de FluxoSobrecarga do Operador de AtribuiçãoSobrecarga dos Operadores [] e ()Operadores de Conversão de TipoRecapitulação - Classe Matrix9Templates
Templates de FunçõesTemplates de ClassesEspecialização de TemplatesTemplates VariádicosNoções Básicas de SFINAE e Traits de TiposRecapitulação - Contêiner Genérico12Recursos Modernos do C++
Semântica de Movimentação e RvaluesEncaminhamento PerfeitoExpressões Lambda em Profundidadestd::function e std::bindconstexpr e constevalVinculações Estruturadasoptional, variant, anyPratique por conta própria: Compilador de C++ online