it-swarm-pt.tech

Como o std :: forward funciona?

Possível Duplicar:
Vantagens de usar o forward

Eu sei o que faz e quando usá-lo, mas eu ainda não pode envolver minha cabeça em torno de como ele funciona. Por favor, seja o mais detalhado possível e explique quando std::forward seria incorreto se fosse permitido usar dedução de argumento de modelo.

Parte da minha confusão é esta: "Se tem um nome, é um lvalue" - se esse é o caso porque o std::forward se comporta de maneira diferente quando eu passo thing&& x vs thing& x?

100
David

Primeiro, vamos dar uma olhada no que std::forward faz de acordo com o padrão:

§20.2.3 [forward] p2

Retorna: static_cast<T&&>(t)

(Onde T é o parâmetro de template explicitamente especificado e t é o argumento passado.)

Agora lembre-se das regras de referência de recolhimento:

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)

(Shamelessly roubado de esta resposta .)

E então vamos dar uma olhada em uma classe que quer empregar o encaminhamento perfeito:

template<class T>
struct some_struct{
  T _v;
  template<class U>
  some_struct(U&& v)
    : _v(static_cast<U&&>(v)) {} // perfect forwarding here
                                 // std::forward is just syntactic sugar for this
};

E agora um exemplo de invocação:

int main(){
  some_struct<int> s1(5);
  // in ctor: '5' is rvalue (int&&), so 'U' is deduced as 'int', giving 'int&&'
  // ctor after deduction: 'some_struct(int&& v)' ('U' == 'int')
  // with rvalue reference 'v' bound to rvalue '5'
  // now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int&&>(v)'
  // this just turns 'v' back into an rvalue
  // (named rvalue references, 'v' in this case, are lvalues)
  // huzzah, we forwarded an rvalue to the constructor of '_v'!

  // attention, real magic happens here
  int i = 5;
  some_struct<int> s2(i);
  // in ctor: 'i' is an lvalue ('int&'), so 'U' is deduced as 'int&', giving 'int& &&'
  // applying the reference collapsing rules yields 'int&' (& + && -> &)
  // ctor after deduction and collapsing: 'some_struct(int& v)' ('U' == 'int&')
  // with lvalue reference 'v' bound to lvalue 'i'
  // now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int& &&>(v)'
  // after collapsing rules: 'static_cast<int&>(v)'
  // this is a no-op, 'v' is already 'int&'
  // huzzah, we forwarded an lvalue to the constructor of '_v'!
}

Espero que esta resposta passo-a-passo ajude você e os outros a entender como o std::forward funciona.

133
Xeo

Eu acho que a explicação de std::forward como static_cast<T&&> é confusa. Nossa intuição para um elenco é que ele converte um tipo em algum outro tipo - nesse caso, seria uma conversão para uma referência de valor. Não é! Então estamos explicando uma coisa misteriosa usando outra coisa misteriosa. Este elenco particular é definido por uma tabela na resposta do Xeo. Mas a pergunta é porque? Então aqui está o meu entendimento:

Suponha que eu queira passar a você um std::vector<T> v que você deveria armazenar em sua estrutura de dados como membro de dados _v. A solução ingênua (e segura) seria sempre copiar o vetor em seu destino final. Então, se você está fazendo isso através de uma função intermediária (método), essa função deve ser declarada como tendo uma referência. (Se você declarar como tendo um vetor por valor, estará realizando uma cópia adicional totalmente desnecessária.)

void set(std::vector<T> & v) { _v = v; }

Tudo bem se você tiver um lvalue em sua mão, mas que tal um valor? Suponha que o vetor seja o resultado de chamar uma função makeAndFillVector(). Se você realizou uma atribuição direta:

_v = makeAndFillVector();

o compilador iria mover o vetor ao invés de copiá-lo. Mas se você introduzir um intermediário, set(), as informações sobre a natureza rvalue do seu argumento seriam perdidas e uma cópia seria feita.

set(makeAndFillVector()); // set will still make a copy

Para evitar essa cópia, você precisa de "encaminhamento perfeito", o que resultaria em um código ideal todas as vezes. Se você receber um lvalue, você quer que sua função trate como um lvalue e faça uma cópia. Se você receber um valor, você quer que sua função trate isso como um valor e mova-o.

Normalmente você faria isso sobrecarregando a função set() separadamente para lvalues ​​e rvalues:

set(std::vector<T> & lv) { _v = v; }
set(std::vector<T> && rv) { _v = std::move(rv); }

Mas agora imagine que você está escrevendo uma função de modelo que aceita T e chama set() com T (não se preocupe com o fato de que set() é apenas definida para vetores). O truque é que você deseja que esse modelo chame a primeira versão de set() quando a função de modelo é instanciada com um lvalue e a segunda quando é inicializada com um rvalue.

Primeiro de tudo, qual deve ser a assinatura desta função? A resposta é esta:

template<class T>
void perfectSet(T && t);

Dependendo de como você chama essa função de modelo, o tipo T será deduzido magicamente de forma diferente. Se você chamá-lo com um lvalue:

std::vector<T> v;
perfectSet(v);

o vetor v será passado por referência. Mas se você o chamar com um rvalue:

perfectSet(makeAndFillVector());

o vetor (anônimo) será passado pela referência rvalue. Portanto, a mágica do C++ 11 é propositadamente configurada de modo a preservar a natureza rvalue dos argumentos, se possível.

Agora, dentro do perfectSet, você quer passar o argumento perfeitamente para a sobrecarga correta de set(). É aqui que o std::forward é necessário:

template<class T>
void perfectSet(T && t) {
    set(std::forward<T>(t));
}

Sem std :: forward, o compilador teria que assumir que queremos passar t por referência. Para se convencer de que isso é verdade, compare este código:

void perfectSet(T && t) {
    set(t);
    set(t); // t still unchanged
}

para isso:

void perfectSet(T && t) {
    set(std::forward<T>(t));
    set(t); // t is now empty
}

Se você não encaminhar explicitamente t, o compilador deve assumir defensivamente que você pode estar acessando t novamente e escolher a versão de referência lvalue do conjunto. Mas se você encaminhar t, o compilador irá preservar o rvalue-ness dele e a versão de referência rvalue de set() será chamada. Esta versão move o conteúdo de t, o que significa que o original fica vazio.

Essa resposta acabou sendo muito mais longa do que eu supus inicialmente ;-)

146
Bartosz Milewski

Ele funciona porque quando o encaminhamento perfeito é invocado, o tipo T é não o tipo de valor, ele também pode ser um tipo de referência.

Por exemplo:

template<typename T> void f(T&&);
int main() {
    std::string s;
    f(s); // T is std::string&
    const std::string s2;
    f(s2); // T is a const std::string&
}

Como tal, forward pode simplesmente olhar o tipo explícito T para ver o que você realmente = passou. É claro que a implementação exata de fazer isso é não-trival, se bem me lembro, mas é aí que está a informação.

Quando você se refere a chamado referência de valor, então isso é realmente um lvalue. No entanto, forward detecta através dos meios acima que é realmente um rvalue e retorna corretamente um rvalue a ser encaminhado.

0
Puppy