Perfect Forwarding
Parte da seção Programação Orientada a Objetos do Journey de C++ da Coddy — lição 84 de 104.
Ao escrever funções de template que aceitam argumentos e os passam para outras funções, surge um problema: como preservar se o argumento original era um lvalue ou rvalue? O Perfect forwarding resolve isso passando os argumentos exatamente como foram recebidos, mantendo sua categoria de valor.
Os ingredientes principais são referências de encaminhamento (escritas como T&& em um contexto de template) e std::forward. Uma referência de encaminhamento pode se vincular tanto a lvalues quanto a rvalues, e std::forward converte condicionalmente o argumento de volta para seu tipo original:
#include <iostream>
#include <utility>
void process(int& x) { std::cout << "lvalue: " << x << "\n"; }
void process(int&& x) { std::cout << "rvalue: " << x << "\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int main() {
int n = 10;
wrapper(n); // Chama process(int&) - lvalue preservado
wrapper(20); // Chama process(int&&) - rvalue preservado
}Sem o std::forward, parâmetros nomeados são sempre lvalues dentro da função, mesmo que um rvalue tenha sido passado. Isso impediria que a semântica de movimento funcionasse corretamente ao encaminhar para construtores ou outras funções.
O perfect forwarding é essencial para escrever funções de fábrica, classes wrapper e qualquer código genérico que precise construir objetos ou invocar funções, preservando a intenção do chamador em relação a movimentações versus cópias.
Desafio
FácilVamos construir uma função de fábrica que demonstra o encaminhamento perfeito (perfect forwarding) ao construir objetos, preservando a categoria de valor dos argumentos passados a ela. Você criará uma função genérica make_object que encaminha argumentos para o construtor de uma classe, garantindo que lvalues sejam copiados e rvalues sejam movidos.
Você organizará seu código em três arquivos:
Widget.h: Defina uma classeWidgetque rastreia como ela recebe seus dados.Seu
Widgetdeve armazenar um nomestd::stringe um valorint. Forneça dois construtores:- Um que recebe
const std::string&(referência lvalue) eint— imprimeWidget constructed (copy): [name] - Um que recebe
std::string&&(referência rvalue) eint— imprimeWidget constructed (move): [name]e move a string
Adicione também um método
display()que imprime[name]: [value].- Um que recebe
Factory.h: Crie sua função de fábrica com encaminhamento perfeito.Escreva uma função de template
make_objectque aceite uma referência de encaminhamento (forwarding reference) para o nome e umintregular para o valor. Usestd::forwardpara passar o argumento do nome para o construtor deWidget, preservando sua categoria de valor original.A função deve retornar o
Widgetconstruído por valor.main.cpp: Leia duas entradas:- Um nome (string)
- Um valor (inteiro)
Demonstre o encaminhamento perfeito criando dois widgets:
- Primeiro, armazene o nome em uma variável local e passe-o para
make_objectcomo um lvalue - Segundo, passe uma string temporária (o mesmo nome concatenado com
"_temp") diretamente paramake_objectcomo um rvalue
Após criar cada widget, chame seu método
display().
Por exemplo, com as entradas Gadget e 42:
Widget constructed (copy): Gadget
Gadget: 42
Widget constructed (move): Gadget_temp
Gadget_temp: 42Com as entradas Device e 100:
Widget constructed (copy): Device
Device: 100
Widget constructed (move): Device_temp
Device_temp: 100A ideia principal é que sua função make_object não sabe se recebeu um lvalue ou rvalue — mas, ao usar std::forward, ela preserva essa informação ao chamar o construtor de Widget. Isso permite que a resolução de sobrecarga do construtor selecione a versão apropriada, permitindo movimentos eficientes para temporários enquanto copia variáveis nomeadas com segurança.
Folha de consulta
O encaminhamento perfeito (perfect forwarding) permite que funções de template passem argumentos para outras funções, preservando sua categoria de valor original (lvalue ou rvalue).
O mecanismo utiliza referências de encaminhamento (T&& em contexto de template) combinadas com std::forward:
#include <utility>
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}Uma referência de encaminhamento pode se vincular tanto a lvalues quanto a rvalues. std::forward<T> converte condicionalmente o argumento de volta ao seu tipo original.
Sem o std::forward, parâmetros nomeados dentro de uma função são sempre tratados como lvalues, mesmo que um rvalue tenha sido passado. Isso impediria que a semântica de movimento (move semantics) funcionasse corretamente.
Exemplo demonstrando a preservação da categoria de valor:
void process(int& x) { std::cout << "lvalue: " << x << "\n"; }
void process(int&& x) { std::cout << "rvalue: " << x << "\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int n = 10;
wrapper(n); // Chama process(int&) - lvalue preservado
wrapper(20); // Chama process(int&&) - rvalue preservadoO encaminhamento perfeito é essencial para funções de fábrica (factory functions), classes wrapper e código genérico que constrói objetos ou invoca funções, preservando a intenção do chamador em relação a movimentos versus cópias.
Experimente você mesmo
#include <iostream>
#include <string>
#include "Widget.h"
#include "Factory.h"
using namespace std;
int main() {
// Ler entrada
string name;
int value;
cin >> name >> value;
// TODO: Criar o primeiro widget passando name como um lvalue
// Armazenar o nome em uma variável local e passá-lo para make_object
// TODO: Chamar display() no primeiro widget
// TODO: Criar o segundo widget passando uma string temporária como um rvalue
// Passar (name + "_temp") diretamente para make_object
// TODO: Chamar display() no segundo widget
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