Você precisa entender o problema de encaminhamento. Você pode ler todo o problema em detalhes , mas vou resumir.
Basicamente, dada a expressão E(a, b, ... , c)
, queremos que a expressão f(a, b, ... , c)
seja equivalente. No C ++ 03, isso é impossível. Existem muitas tentativas, mas todas falham em alguns aspectos.
O mais simples é usar uma referência lvalue:
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
E(a, b, c);
}
Mas isso falha ao manipular valores temporários:, f(1, 2, 3);
pois esses não podem ser vinculados a uma referência de valor l.
A próxima tentativa pode ser:
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
E(a, b, c);
}
Que corrige o problema acima, mas vira flops. Agora ele deixa de permitir E
argumentos não-const:
int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these
A terceira tentativa aceita referências const, mas depois const_cast
é a const
distância:
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}
Isso aceita todos os valores, pode transmitir todos os valores, mas potencialmente leva a um comportamento indefinido:
const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!
Uma solução final lida com tudo corretamente ... ao custo de ser impossível de manter. Você fornece sobrecargas de f
, com todas as combinações de const e non-const:
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);
template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);
template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);
template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);
template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);
template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);
template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);
N argumentos requerem 2 N combinações de , um pesadelo. Gostaríamos de fazer isso automaticamente.
(Isso é efetivamente o que o compilador faz por nós no C ++ 11.)
No C ++ 11, temos a chance de corrigir isso. Uma solução modifica as regras de dedução de modelos nos tipos existentes, mas isso potencialmente quebra uma grande quantidade de código.Então, temos que encontrar outro caminho.
A solução é, em vez disso, usar as referências-rvalue recém-adicionadas ; podemos introduzir novas regras ao deduzir os tipos de referência rvalue e criar qualquer resultado desejado. Afinal, não podemos possivelmente quebrar o código agora.
Se for dada uma referência a uma referência (note reference é um termo abrangente que significa ambos T&
e T&&
), usamos a seguinte regra para descobrir o tipo resultante:
"[dado] um tipo TR que é uma referência a um tipo T, uma tentativa de criar o tipo" referência de valor para cv TR "cria o tipo" referência de valor para T ", enquanto uma tentativa de criar o tipo" referência de valor para cv TR ”cria o tipo TR".
Ou em forma de tabela:
TR R
T& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T& && -> T& // rvalue reference to cv TR -> TR (lvalue reference to T)
T&& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T&& && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)
Em seguida, com dedução de argumento do modelo: se um argumento é um valor l, A, fornecemos ao argumento do modelo uma referência lvalue a A. Caso contrário, deduzimos normalmente. Isso fornece as chamadas referências universais (o termo referência de encaminhamento agora é oficial).
Por que isso é útil? Como combinados, mantemos a capacidade de acompanhar a categoria de valor de um tipo: se fosse um lvalue, teremos um parâmetro lvalue-reference, caso contrário, teremos um parâmetro rvalue-reference.
Em código:
template <typename T>
void deduce(T&& x);
int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)
A última coisa é "encaminhar" a categoria de valor da variável. Lembre-se de que, uma vez dentro da função, o parâmetro pode ser passado como um valor lvalue para qualquer coisa:
void foo(int&);
template <typename T>
void deduce(T&& x)
{
foo(x); // fine, foo can refer to x
}
deduce(1); // okay, foo operates on x which has a value of 1
Isso não é bom. E precisa obter o mesmo tipo de categoria de valor que recebemos! A solução é esta:
static_cast<T&&>(x);
O que isso faz? Considere que estamos dentro da deduce
função e recebemos um lvalue. Isso significa que T
é a A&
e, portanto, o tipo de destino para o elenco estático é A& &&
, ou apenas A&
. Como x
já é um A&
, não fazemos nada e somos deixados com uma referência lvalue.
Quando passamos um rvalue, T
é A
, então o tipo de destino para a conversão estática é A&&
. A conversão resulta em uma expressão rvalue, que não pode mais ser passada para uma referência lvalue . Mantemos a categoria de valor do parâmetro.
Juntar isso nos dá "encaminhamento perfeito":
template <typename A>
void f(A&& a)
{
E(static_cast<A&&>(a));
}
Quando f
recebe um lvalue, E
obtém um lvalue. Quando f
recebe um rvalue, E
obtém um rvalue. Perfeito.
E, claro, queremos nos livrar dos feios. static_cast<T&&>
é enigmático e estranho de lembrar; vamos criar uma função utilitária chamada forward
, que faz a mesma coisa:
std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
f
seria uma função, e não uma expressão?const int i
será aceito:A
será deduzido aconst int
. As falhas são para os literais rvalues. Observe também que, para a chamada paradeduced(1)
, x éint&&
, nãoint
(o encaminhamento perfeito nunca faz uma cópia, como seria feito sex
fosse um parâmetro por valor). MeramenteT
éint
. O motivo quex
avalia um lvalue no encaminhador é porque as referências rvalue nomeadas se tornam expressões lvalue.forward
oumove
aqui? Ou é apenas uma diferença semântica?std::move
deve ser chamado sem argumentos explícitos do modelo e sempre resulta em um rvalue, enquantostd::forward
pode acabar como um. Usestd::move
quando você sabe que não precisa mais do valor e deseja movê-lo para outro lugar, usestd::forward
para fazer isso de acordo com os valores passados para o seu modelo de função.Eu acho que ter um código conceitual implementando std :: forward pode adicionar à discussão. Este é um slide de Scott Meyers talk Um exemplo eficaz de C ++ 11/14
Função
move
no código éstd::move
. Existe uma implementação (de trabalho) para ela no início dessa conversa. Eu encontrei a implementação real do std :: forward no libstdc ++ , no arquivo move.h, mas não é de todo instrutivo.Da perspectiva do usuário, o significado disso é que
std::forward
é uma conversão condicional para um rvalor. Pode ser útil se eu estiver escrevendo uma função que espera um lvalue ou rvalue em um parâmetro e deseje passá-la para outra função como um rvalue apenas se ela foi passada como um rvalue. Se eu não envolver o parâmetro em std :: forward, ele sempre será passado como uma referência normal.Com certeza, ele imprime
O código é baseado em um exemplo da palestra mencionada anteriormente. Slide 10, aproximadamente às 15:00 desde o início.
fonte
Se você usar uma referência nomeada rvalue em uma expressão, na verdade, é um lvalue (porque você se refere ao objeto pelo nome). Considere o seguinte exemplo:
Agora, se chamarmos
outer
assimgostaríamos que 17 e 29 fossem encaminhados para o número 2 porque 17 e 29 são literais inteiros e, como tais, rvalores. Mas como
t1
et2
na expressãoinner(t1,t2);
são lvalues, você invocaria o número 1 em vez do número 2. É por isso que precisamos transformar as referências novamente em referências sem nomestd::forward
. Portanto,t1
inouter
é sempre uma expressão lvalue, enquantoforward<T1>(t1)
pode ser uma expressão rvalue, dependendoT1
. A última é apenas uma expressão lvalue seT1
for uma referência lvalue. ET1
é deduzido apenas como uma referência lvalue no caso de o primeiro argumento para externo ser uma expressão lvalue.fonte
Se, após instanciar,
T1
for do tipochar
eT2
de uma classe, você deseja passart1
por cópia et2
porconst
referência. Bem, a menos que osinner()
leve por nãoconst
referência, ou seja, nesse caso, você também deseja fazê-lo.Tente escrever um conjunto de
outer()
funções que implementem isso sem referências a rvalue, deduzindo o caminho certo para passar os argumentos doinner()
tipo de. Eu acho que você precisará de algo 2 ^ 2 deles, coisas bastante pesadas de modelo-meta para deduzir os argumentos e muito tempo para acertar isso em todos os casos.E então alguém vem com um
inner()
que recebe argumentos por ponteiro. Eu acho que agora faz 3 ^ 2. (Ou 4 ^ 2. Inferno, não me incomodo em tentar pensar se oconst
ponteiro faria alguma diferença.)E então imagine que você quer fazer isso por cinco parâmetros. Ou sete.
Agora você sabe por que algumas mentes brilhantes criaram um "encaminhamento perfeito": isso faz o compilador fazer tudo isso por você.
fonte
Um ponto que não foi esclarecido é que ele também
static_cast<T&&>
lidaconst T&
adequadamente.Programa:
Produz:
Observe que 'f' deve ser uma função de modelo. Se apenas definido como 'void f (int && a)', isso não funciona.
fonte
Vale a pena enfatizar que o encaminhamento deve ser usado em conjunto com um método externo com encaminhamento / referência universal. Usar o encaminhamento por si só como as instruções a seguir é permitido, mas não serve para nada além de causar confusão. O comitê padrão pode querer desativar essa flexibilidade, caso contrário, por que não usamos apenas static_cast?
Na minha opinião, avançar e avançar são padrões de design que são resultados naturais após a introdução do tipo de referência r-value. Não devemos nomear um método, assumindo que ele seja usado corretamente, a menos que o uso incorreto seja proibido.
fonte