¿Puedo usar normalmente / siempre std:: forward en lugar de std:: move?


He estado viendo la charla de Scott Meyers sobre Referencias Universales de la conferencia de C++ y más allá de 2012, y todo tiene sentido hasta ahora. Sin embargo, un miembro de la audiencia hace una pregunta alrededor de 50 minutos en la que también me preguntaba. Meyers dice que no le importa la respuesta porque no es idiomática y sería tonto su mente, pero todavía estoy interesado.

El código presentado es el siguiente:

// Typical function bodies with overloading:
void doWork(const Widget& param)   // copy
{
  // ops and exprs using param
}
void doWork(Widget&& param)        // move
{
  // ops and exprs using std::move(param)
}

// Typical function implementations with universal reference:
template <typename T>
void doWork(T&& param)             // forward => copy and move
{
  // ops and exprs using std::forward<T>(param)
}

El punto es que cuando tomamos un rvalue referencia, sabemos que tenemos un rvalue, por lo que debemos std::move para preservar el hecho de que es un rvalue. Cuando tomamos una referencia universal (T&&, donde T es un tipo deducido), queremos std::forward preservar el hecho de que puede haber sido un lvalue o un rvalue.

Así que la pregunta es: dado que std::forward conserva si el valor pasado a la función era un lvalue o un rvalue, y std::move simplemente arroja su argumento a un rvalue, ¿podríamos usar std::forward en todas partes? Would std::forward comportarse como std::move en todos los casos donde usaríamos std::move, o hay algunas diferencias importantes en el comportamiento que se pierden por la generalización de Meyers?

No estoy sugiriendo que nadie debería hacerlo porque, como dice correctamente Meyers, es completamente no idiomático, pero es el siguiente también un uso válido de std::move:

void doWork(Widget&& param)         // move
{
  // ops and exprs using std::forward<Widget>(param)
}
Author: Joseph Mansfield, 2012-11-04

2 answers

Los dos son muy diferentes y herramientas complementarias.

  • std::move deduce el argumento y crea incondicionalmente una expresión rvalue. Esto tiene sentido para aplicar a un objeto o variable real.

  • std::forward toma un argumento de plantilla obligatorio (¡debe especificar esto!) y mágicamente crea una expresión lvalue o una rvalue dependiendo del tipo (en virtud de agregar && y las reglas de colapso). Esto solo tiene sentido para aplicar a un argumento de función plantilla deducido.

Tal vez los siguientes ejemplos ilustran esto un poco mejor:

#include <utility>
#include <memory>
#include <vector>
#include "foo.hpp"

std::vector<std::unique_ptr<Foo>> v;

template <typename T, typename ...Args>
std::unique_ptr<T> make_unique(Args &&... args)
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));  // #1
}

int main()
{
    {
        std::unique_ptr<Foo> p(new Foo('a', true, Bar(1,2,3)));
        v.push_back(std::move(p));                                  // #2
    }

    {
        v.push_back(make_unique<Foo>('b', false, Bar(5,6,7)));      // #3
    }

    {
        Bar b(4,5,6);
        char c = 'x';
        v.push_back(make_unique<Foo>(c, b.ready(), b));             // #4
    }
}

En la situación #2, tenemos un objeto concreto p existente, y queremos movernos de él, incondicionalmente. Solo std::move tiene sentido. No hay nada que "adelantar" aquí. Tenemos una variable con nombre y queremos movernos de ella.

Por otro lado, la situación # 1 acepta una lista de cualquier tipo de argumentos, y cada argumento debe ser reenviado como la misma categoría de valor que en la llamada original. Por ejemplo, en #3 los argumentos son expresiones temporales, y por lo tanto se enviarán como rvalues. Pero también podríamos haber mezclado objetos con nombre en la llamada del constructor, como en la situación # 4, y luego necesitamos reenvío como lvalues.

 68
Author: Kerrek SB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-10-03 22:35:49

Sí, si param es un Widget&&, entonces las siguientes tres expresiones son equivalentes (suponiendo que Widget no es un tipo de referencia):

std::move(param)
std::forward<Widget>(param)
static_cast<Widget&&>(param)

En general (cuando Widget puede ser una referencia), {[7] } es equivalente a las dos expresiones siguientes:

std::forward<std::remove_reference<Widget>::type>(param)
static_cast<std::remove_reference<Widget>::type&&>(param)

Tenga en cuenta cuánto mejor std::move es para mover cosas. El punto de std::forward es que se mezcla bien con las reglas de deducción de tipo de plantilla:

template<typename T>
void foo(T&& t) {
    std::forward<T>(t);
    std::move(t);
}

int main() {
    int a{};
    int const b{};
               //Deduced T   Signature    Result of `forward<T>` Result of `move`
    foo(a);    //int&        foo(int&)       lvalue int          xvalue int
    foo(b);    //int const&  foo(int const&) lvalue int const    xvalue int const
    foo(int{});//int         foo(int&&)      xvalue int          xvalue int
}
 14
Author: Mankarse,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-02-24 03:48:39