¿Cuándo usar std :: forward para reenviar argumentos?

155

C ++ 0x muestra un ejemplo de uso std::forward:

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

¿Cuándo es ventajoso usar std::forward, siempre?

Además, requiere su uso &&en la declaración de parámetros, ¿es válido en todos los casos? Pensé que tenía que pasar temporarios a una función si la función se declaraba con &&ella, por lo que ¿se puede llamar a foo con algún parámetro?

Por último, si tengo una llamada de función como esta:

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

Debería usar esto en su lugar:

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

Además, si usa los parámetros dos veces en la función, es decir, reenvío a dos funciones al mismo tiempo, ¿es conveniente usarlo std::forward? ¿No std::forwardconvertirá lo mismo en temporal dos veces, moviendo la memoria y haciendo que no sea válida para un segundo uso? ¿Estaría bien el siguiente código?

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

Estoy un poco confundido std::forwardy con mucho gusto usaría algo de limpieza.

coyotte508
fuente

Respuestas:

124

Úselo como su primer ejemplo:

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

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

Esto se debe a las reglas de colapso de referencia : si T = U&, entonces T&& = U&, pero si T = U&&, entonces T&& = U&&, siempre terminas con el tipo correcto dentro del cuerpo de la función. Finalmente, debe forwardconvertir lvalue-turn x(¡porque tiene un nombre ahora!) De nuevo en una referencia de rvalue si fue uno inicialmente.

Sin embargo, no debe reenviar algo más de una vez, porque eso generalmente no tiene sentido: reenviar significa que potencialmente está moviendo el argumento hasta la última llamada, y una vez que se mueve, desaparece, por lo que no puede usarlo de nuevo (en la forma en que probablemente quisiste hacerlo).

Kerrek SB
fuente
Pensé que era Args...&& args?
Cachorro
55
@DeadMG: Siempre es el correcto, no el que recordaba mal :-) ... ¡aunque en este caso parece que lo recuerdo mal!
Kerrek SB
1
Pero, ¿cómo se declara g para el tipo genérico T?
MK.
@MK. g se declara como una función regular con los parámetros que desea.
CoffeDeveloper
1
@cmdLP: Tienes razón en que está bien definido reenviar repetidamente, pero rara vez es semánticamente correcto para tu programa. Sin embargo, tomar miembros de expresiones directas es un caso útil. Actualizaré la respuesta.
Kerrek SB
4

La respuesta de Kerrek es muy útil, pero no responde completamente la pregunta del título:

¿Cuándo usar std :: forward para reenviar argumentos?

Para responderlo, primero debemos introducir una noción de referencias universales . Scott Meyers dio este nombre y hoy en día a menudo se les llama referencias de reenvío. Básicamente, cuando ves algo como esto:

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

tenga en cuenta que paramno se trata de una referencia de valor (como se puede tentar a concluir), sino de una referencia universal *. Las referencias universales se caracterizan por una forma muy restringida (solo T&&, sin const o calificadores similares) y por deducción de tipo : el tipo Tse deducirá cuando fse invoque. En pocas palabras, las referencias universales corresponden a las referencias de valor si se inicializan con valores, y a las referencias de valor si se inicializan con valores.

Ahora es relativamente fácil responder la pregunta original: solicítela std::forwarda:

  • una referencia universal la última vez que se usó en la función
  • una referencia universal que se devuelve desde funciones que regresan por valor

Un ejemplo para el primer 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
}

En el código anterior, no queremos proptener algún valor desconocido después de que other.set(..)haya terminado, por lo que no se realiza ningún reenvío aquí. Sin embargo, cuando barllamamos hacia adelante, propya que hemos terminado con él y barpodemos hacer lo que quiera con él (por ejemplo, moverlo).

Un ejemplo para el segundo caso:

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

Esta plantilla de función debe moverse propal valor de retorno si es un valor r y copiarlo si es un valor l. En caso de que omitimos std::forwardal final, siempre creamos una copia, que es más costosa cuando propresulta ser un valor.

* para ser completamente preciso, una referencia universal es un concepto de tomar una referencia rvalue a un parámetro de plantilla no calificado cv.

Miljen Mikic
fuente
0

¿Ayuda este ejemplo? Luché por encontrar un ejemplo no genérico útil de std :: forward, pero me topé con un ejemplo de una cuenta bancaria en la que pasamos el efectivo para ser depositado como argumento.

Entonces, si tenemos una versión constante de una cuenta, deberíamos esperar cuando la pasamos a nuestra plantilla de depósito <> que se llama a la función constante; y esto arroja una excepción (¡la idea es que se trataba de una cuenta bloqueada!)

Si tenemos una cuenta no constante, entonces deberíamos poder modificar la cuenta.

#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;
    }
}

Para 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

Rendimiento esperado:

# 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
fuente