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?