Vantagens de usar para a frente


437

No encaminhamento perfeito, std::forwardé usado para converter as referências rvalue nomeadas t1e t2em referências rvalue não nomeadas. Qual é o propósito de fazer isso? Como isso afetaria a função chamadainner se deixarmos t1& t2como lvalues?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Respostas:


788

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 Eargumentos 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 constdistâ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 deducefunçã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 xjá é 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 frecebe um lvalue, Eobtém um lvalue. Quando frecebe um rvalue, Eobté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);

1
Não fseria uma função, e não uma expressão?
Michael Foukarakis

1
Sua última tentativa não está correta com relação à declaração do problema: ela encaminhará os valores const como não-const, portanto não será encaminhada. Observe também que, na primeira tentativa, const int iserá aceito: Aserá deduzido a const int. As falhas são para os literais rvalues. Observe também que, para a chamada para deduced(1), x é int&&, não int(o encaminhamento perfeito nunca faz uma cópia, como seria feito se xfosse um parâmetro por valor). Meramente Té int. O motivo que xavalia um lvalue no encaminhador é porque as referências rvalue nomeadas se tornam expressões lvalue.
Johannes Schaub - litb 27/08

5
Existe alguma diferença no uso forwardou moveaqui? Ou é apenas uma diferença semântica?
0x499602D2

28
@ David: std::movedeve ser chamado sem argumentos explícitos do modelo e sempre resulta em um rvalue, enquanto std::forwardpode acabar como um. Use std::movequando você sabe que não precisa mais do valor e deseja movê-lo para outro lugar, use std::forwardpara fazer isso de acordo com os valores passados ​​para o seu modelo de função.
GManNickG

5
Obrigado por começar primeiro com exemplos concretos e motivar o problema; muito útil!
precisa saber é o seguinte

60

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

código conceitual implementando std :: forward

Função moveno 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.

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Com certeza, ele imprime

std::string& version
std::string&& version

O código é baseado em um exemplo da palestra mencionada anteriormente. Slide 10, aproximadamente às 15:00 desde o início.


2
Seu segundo link acabou apontando para um lugar completamente diferente.
Pharap

32

No encaminhamento perfeito, std :: forward é usado para converter a referência rvalue nomeada t1 e t2 em referência rvalue não nomeada. Qual é o propósito de fazer isso? Como isso afetaria a função chamada interior se deixarmos t1 & t2 como lvalue?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

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:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Agora, se chamarmos outerassim

outer(17,29);

gostarí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 t1e t2na expressão inner(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 nome std::forward. Portanto, t1in outeré sempre uma expressão lvalue, enquanto forward<T1>(t1)pode ser uma expressão rvalue, dependendo T1. A última é apenas uma expressão lvalue se T1for uma referência lvalue. E T1é deduzido apenas como uma referência lvalue no caso de o primeiro argumento para externo ser uma expressão lvalue.


Este é um tipo de explicação atenuada, mas muito bem feita e funcional. As pessoas devem ler esta resposta em primeiro lugar e, em seguida, ir mais fundo, se desejar
NicoBerrogorry

11

Como isso afetaria a função chamada inner se deixarmos t1 & t2 como lvalue?

Se, após instanciar, T1for do tipo chare T2de uma classe, você deseja passar t1por cópia e t2por constreferência. Bem, a menos que os inner()leve por não constreferê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 do inner()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 o constponteiro 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ê.


4

Um ponto que não foi esclarecido é que ele também static_cast<T&&>lida const T&adequadamente.
Programa:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

Produz:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Observe que 'f' deve ser uma função de modelo. Se apenas definido como 'void f (int && a)', isso não funciona.


ponto positivo, então o T&& no elenco estático também segue as regras de recolhimento de referência, certo?
Barney

2

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?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

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.


Eu não acho que o comitê do C ++ sinta que a responsabilidade é deles usar os idiomas da linguagem "corretamente", nem definir o que é o uso "correto" (embora eles certamente possam dar diretrizes). Para esse fim, embora os professores, chefes e amigos de uma pessoa possam ter o dever de orientá-los de uma maneira ou de outra, acredito que o comitê C ++ (e, portanto, o padrão) não tem esse dever.
SirGuy

Sim, acabei de ler a N2951 e concordo que o comitê padrão não tem obrigação de adicionar limitações desnecessárias ao uso de uma função. Mas os nomes desses dois modelos de função (mover e avançar) são realmente um pouco confusos, visto apenas suas definições no arquivo da biblioteca ou na documentação padrão (23.2.5 Auxiliares de avanço / movimentação). Os exemplos no padrão definitivamente ajudam a entender o conceito, mas pode ser útil adicionar mais comentários para tornar as coisas um pouco mais claras.
Colin
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.