Quando usar std :: forward para encaminhar argumentos?

155

C ++ 0x mostra um exemplo de uso std::forward:

template<class T>
void foo(T&& arg) 
{
  bar(std::forward<T>(arg));
}

Quando é vantajoso usar std::forwardsempre?

Além disso, ele requer o uso &&na declaração de parâmetros, é válido em todos os casos? Eu pensei que você tinha que passar temporários para uma função se a função foi declarada com &&ela, então pode ser chamado com qualquer parâmetro?

Por fim, se eu tiver uma chamada de função como esta:

template<int val, typename... Params>
void doSomething(Params... args) {
  doSomethingElse<val, Params...>(args...);
}

Devo usar isso em seu lugar:

template<int val, typename... Params>
void doSomething(Params&&... args) {
  doSomethingElse<val, Params...>(std::forward<Params>(args)...);
}

Além disso, se usar os parâmetros duas vezes na função, ou seja, encaminhar para duas funções ao mesmo tempo, é aconselhável usar std::forward? Não std::forwardconverterá a mesma coisa em temporária duas vezes, movendo a memória e a invalidará para um segundo uso? O código a seguir está ok:

template<int val, typename... Params>
void doSomething(Params&&... args) {
  doSomethingElse<val, Params...>(std::forward<Params>(args)...);
  doSomethingWeird<val, Params...>(std::forward<Params>(args)...);
}

Estou um pouco confuso std::forward, e eu ficaria feliz em esclarecer.

coyotte508
fonte

Respostas:

124

Use-o como seu primeiro exemplo:

template <typename T> void f(T && x)
{
  g(std::forward<T>(x));
}

template <typename ...Args> void f(Args && ...args)
{
  g(std::forward<Args>(args)...);
}

Isso ocorre por causa das regras de recolhimento de referência : Se T = U&, então T&& = U&, mas se T = U&&, então T&& = U&&, você sempre acaba com o tipo correto dentro do corpo da função. Por fim, você precisa forwardtransformar o lvalue-virou x(porque ele tem um nome agora!) De volta em uma referência de rvalue, se fosse um inicialmente.

Você não deve encaminhar algo mais de uma vez, no entanto, porque isso geralmente não faz sentido: encaminhar significa que você está potencialmente movendo o argumento até o chamador final e, depois que ele é movido, desaparece, então você não pode usá-lo novamente (da maneira que você provavelmente pretendia).

Kerrek SB
fonte
Eu pensei que era Args...&& args?
Filhote de cachorro
5
@DeadMG: É sempre o que está correto, não o que eu me lembrei :-) ... embora neste caso pareço ter me lembrado corretamente!
Kerrek SB
1
Mas como é declarado g para o tipo T genérico?
MK.
@MK. g é declarado como uma função regular com os parâmetros desejados.
CoffeDeveloper
1
@cmdLP: Você está certo de que está bem definido para encaminhar repetidamente, mas raramente é semanticamente correto para o seu programa. Porém, aceitar membros de expressões avançadas é um caso útil. Vou atualizar a resposta.
Kerrek SB 6/06/19
4

A resposta de Kerrek é muito útil, mas não responde completamente à pergunta do título:

Quando usar std :: forward para encaminhar argumentos?

Para responder, devemos primeiro introduzir uma noção de referências universais . Scott Meyers deu esse nome e hoje em dia é chamado de referência de encaminhamento. Basicamente, quando você vê algo assim:

template<typename T>
void f(T&& param);

tenha em mente que paramnão é uma referência de valor (como alguém pode ser tentado a concluir), mas uma referência universal *. As referências universais são caracterizadas por uma forma muito restrita (apenas T&&, sem qualificadores const ou similares) e por dedução de tipo - o tipo Tserá deduzido quando ffor invocado. Em poucas palavras, as referências universais correspondem às referências rvalue se forem inicializadas com rvalues, e às referências lvalue se forem inicializadas com lvalues.

Agora é relativamente fácil responder à pergunta original - aplique std::forwarda:

  • uma referência universal da última vez que é usada na função
  • uma referência universal retornada de funções que retornam por valor

Um exemplo para o primeiro caso:

template<typename T>
void foo(T&& prop) {
    other.set(prop); // use prop, but don't modify it because we still need it
    bar(std::forward<T>(prop)); // final use -> std::forward
}

No código acima, não queremos propter um valor desconhecido após a other.set(..)conclusão, portanto, nenhum encaminhamento acontece aqui. No entanto, ao ligar bar, encaminhamos propcomo já terminamos e barpodemos fazer o que quiser com ele (por exemplo, movê-lo).

Um exemplo para o segundo caso:

template<typename T>
Widget transform(T&& prop) {
   prop.transform();
   return std::forward<T>(prop);
}

Este modelo de função deve passar proppara o valor de retorno se for um rvalue e copiá-lo se for um lvalue. No caso de omitirmos std::forwardno final, sempre criaríamos uma cópia, que é mais cara quando propocorre um rvalor.

* para ser totalmente preciso, uma referência universal é um conceito de obter uma referência rvalue para um parâmetro de modelo não qualificado por cv.

Miljen Mikic
fonte
0

Este exemplo ajuda? Lutei para encontrar um exemplo útil e não genérico de std :: forward, mas encontrei um exemplo de conta bancária que repassamos ao longo do dinheiro para ser depositado como argumento.

Portanto, se tivermos uma versão const de uma conta, esperamos que, quando passamos para o nosso modelo de depósito <>, que a função const seja chamada; e isso gera uma exceção (a ideia era que era uma conta bloqueada!)

Se tivermos uma conta não const, poderemos modificar a conta.

#include <iostream>
#include <string>
#include <sstream> // std::stringstream
#include <algorithm> // std::move
#include <utility>
#include <iostream>
#include <functional>

template<class T> class BankAccount {
private:
    const T no_cash {};
    T cash {};
public:
    BankAccount<T> () {
        std::cout << "default constructor " << to_string() << std::endl;
    }
    BankAccount<T> (T cash) : cash (cash) {
        std::cout << "new cash " << to_string() << std::endl;
    }
    BankAccount<T> (const BankAccount& o) {
        std::cout << "copy cash constructor called for " << o.to_string() << std::endl;
        cash = o.cash;
        std::cout << "copy cash constructor result is  " << to_string() << std::endl;
    }
    // Transfer of funds?
    BankAccount<T> (BankAccount<T>&& o) {
        std::cout << "move cash called for " << o.to_string() << std::endl;
        cash = o.cash;
        o.cash = no_cash;
        std::cout << "move cash result is  " << to_string() << std::endl;
    }
    ~BankAccount<T> () {
        std::cout << "delete account " << to_string() << std::endl;
    }
    void deposit (const T& deposit) {
        cash += deposit;
        std::cout << "deposit cash called " << to_string() << std::endl;
    }
    friend int deposit (int cash, const BankAccount<int> &&account) {
        throw std::string("tried to write to a locked (const) account");
    }
    friend int deposit (int cash, const BankAccount<int> &account) {
        throw std::string("tried to write to a locked (const) account");
    }
    friend int deposit (int cash, BankAccount<int> &account) {
        account.deposit(cash);
        return account.cash;
    }
    friend std::ostream& operator<<(std::ostream &os, const BankAccount<T>& o) {
        os << "$" << std::to_string(o.cash);
        return os;
    }
    std::string to_string (void) const {
        auto address = static_cast<const void*>(this);
        std::stringstream ss;
        ss << address;
        return "BankAccount(" + ss.str() + ", cash $" + std::to_string(cash) + ")";
    }
};

template<typename T, typename Account>
int process_deposit(T cash, Account&& b) {
    return deposit(cash, std::forward<Account>(b));
}

int main(int, char**)
{
    try {
        // create account1 and try to deposit into it
        auto account1 = BankAccount<int>(0);
        process_deposit<int>(100, account1);
        std::cout << account1.to_string() << std::endl;
        std::cout << "SUCCESS: account1 deposit succeeded!" << std::endl;
    } catch (const std::string &e) {
        std::cerr << "FAILED: account1 deposit failed!: " << e << std::endl;
    }

    try {
        // create locked account2 and try to deposit into it; this should fail
        const auto account2 = BankAccount<int>(0);
        process_deposit<int>(100, account2);
        std::cout << account2.to_string() << std::endl;
        std::cout << "SUCCESS: account2 deposit succeeded!" << std::endl;
    } catch (const std::string &e) {
        std::cerr << "FAILED: account2 deposit failed!: " << e << std::endl;
    }

    try {
        // create locked account3 and try to deposit into it; this should fail
        auto account3 = BankAccount<int>(0);
        process_deposit<int>(100, std::move(account3));
        std::cout << account3.to_string() << std::endl;
        std::cout << "SUCCESS: account3 deposit succeeded!" << std::endl;
    } catch (const std::string &e) {
        std::cerr << "FAILED: account3 deposit failed!: " << e << std::endl;
    }
}

Construir:

cd std_forward
rm -f *.o example
c++ -std=c++2a -Werror -g -ggdb3 -Wall -c -o main.o main.cpp
c++ main.o  -o example
./example

Saída esperada:

# create account1 and try to deposit into it
new cash BankAccount(0x7ffee68d96b0, cash $0)
deposit cash called BankAccount(0x7ffee68d96b0, cash $100)
BankAccount(0x7ffee68d96b0, cash $100)
# SUCCESS: account1 deposit succeeded!
delete account BankAccount(0x7ffee68d96b0, cash $100)

# create locked account2 and try to deposit into it; this should fail
new cash BankAccount(0x7ffee68d9670, cash $0)
delete account BankAccount(0x7ffee68d9670, cash $0)
# FAILED: account2 deposit failed!: tried to write to a locked (const) account

# create locked account3 and try to deposit into it; this should fail
new cash BankAccount(0x7ffee68d9630, cash $0)
delete account BankAccount(0x7ffee68d9630, cash $0)
# FAILED: account3 deposit failed!: tried to write to a locked (const) account
Neil McGill
fonte