¿Por qué utilizar un valor perfectamente reenviado (un funtor)?


C++11 (y C++14) introduce construcciones de lenguaje adicionales y mejoras que se dirigen a la programación genérica. Estos incluyen características tales como;

  • Referencias de valores R
  • Colapso de referencia
  • Perfecto reenvío
  • Mover semántica, plantillas variádicas y más

Estaba hojeando un anterior borrador de la especificación C++14 (ahora con texto actualizado) y el código en un ejemplo en §20.5.1, Entero en tiempo de compilación secuencias, que encontré interesantes y peculiares.

template<class F, class Tuple, std::size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
  return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(t))...);
}

template<class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
  using Indices = make_index_sequence<std::tuple_size<Tuple>::value>;
  return apply_impl(std::forward<F>(f), std::forward<Tuple>(t), Indices());
}

En línea aquí [intseq.general] / 2 .

Pregunta

  • ¿Por qué se reenvía la función f en apply_impl, es decir, por qué std::forward<F>(f)(std::get...?
  • ¿Por qué no aplicar la función como f(std::get...?
Author: Niall, 2014-07-16

2 answers

En Breve...

El TL;DR, desea preservar la categoría de valor (naturaleza de valor r/valor l) del funtor porque esto puede afectar la resolución de sobrecarga , en particular los miembros calificados como ref.

Reducción de la definición de función

Para centrarme en la cuestión de la función que se reenvía, he reducido el ejemplo (y lo he hecho compilar con un compilador C++11) a;

template<class F, class... Args>
auto apply_impl(F&& func, Args&&... args) -> decltype(std::forward<F>(func)(std::forward<Args>(args)...)) {
  return std::forward<F>(func)(std::forward<Args>(args)...);
}

Y creamos una segunda forma, donde reemplazamos el std::forward(func) con solo func;

template<class F, class... Args>
auto apply_impl_2(F&& func, Args&&... args) -> decltype(func(std::forward<Args>(args)...)) {
  return func(std::forward<Args>(args)...);
}

Evaluación de la muestra

Evaluar alguna evidencia empírica de cómo se comporta esto (con compiladores conformes) es un buen punto de partida para evaluar por qué el ejemplo de código fue escrito como tal. Por lo tanto, además vamos a definir un funtor general;

struct Functor1 {
  int operator()(int id) const
  {
    std::cout << "Functor1 ... " << id << std::endl;
    return id;
  }
};

Muestra Inicial

Ejecuta un código de ejemplo;

int main()
{
  Functor1 func1;
  apply_impl_2(func1, 1);
  apply_impl_2(Functor1(), 2);
  apply_impl(func1, 3);
  apply_impl(Functor1(), 4);
}

Y la salida es la esperada, independientemente de si se utiliza un valor r Functor1() o un valor l func al hacer la llamada a apply_impl y apply_impl_2 se llama al operador de llamada sobrecargado. Se llama tanto para los valores r como para los valores l. Bajo C++03, esto era todo lo que tenía, no podía sobrecargar los métodos de miembros basados en el" valor-r "o" valor-l " del objeto.

Funtor1 ... 1
Funtor1 ... 2
Funtor1 ... 3
Funtor1 ... 4

Ref-qualified samples

Ahora necesitamos sobrecargar ese operador de llamada para estirar este a poco más...

struct Functor2 {
  int operator()(int id) const &
  {
    std::cout << "Functor2 &... " << id << std::endl;
    return id;
  }
  int operator()(int id) &&
  {
    std::cout << "Functor2 &&... " << id << std::endl;
    return id;
  }
};

Ejecutamos otro conjunto de muestras;

int main()
{
  Functor2 func2;
  apply_impl_2(func2, 5);
  apply_impl_2(Functor2(), 6);
  apply_impl(func2, 7);
  apply_impl(Functor2(), 8);
}

Y la salida es;

Functor2&... 5
Functor2 &... 6
Functor2 &... 7
Functor2&&... 8

Discusión

En el caso de apply_impl_2 (id 5 y 6), el resultado no es como se esperaba inicialmente. En ambos casos, se llama al valor-l calificado operator() (el valor-r no se llama en absoluto). Se puede haber esperado que dado que Functor2(), un valor r, se usa para llamar a apply_impl_2, se habría llamado al valor r calificado operator(). El func, como un parámetro con nombre para apply_impl_2, es una referencia de valor r, pero como se nombra, es en sí mismo un valor l. Por lo tanto, el valor-l calificado operator()(int) const& se llama tanto en el caso del valor-l func2 siendo el argumento y el valor-r Functor2() siendo utilizado como el argumento.

En el caso de apply_impl (id 7 y 8) el std::forward<F>(func) mantiene o preserva la naturaleza del valor-r/valor-l de la argumentación prevista para func. Por lo tanto, el valor-l calificado operator()(int) const& se llama con el valor-l func2 utilizado como argumento y el valor-r calificado operator()(int)&& cuando el valor-r Functor2() se utiliza como argumento. Este comportamiento es lo que se hubiera esperado.

Conclusiones

El uso de std::forward, vía perfect forwarding, asegura que preservamos la naturaleza del valor r/valor l del argumento original para func. Conserva su valor categoría.

Es necesario, std::forward puede y debe usarse para algo más que reenviar argumentos a funciones, pero también cuando se requiere el uso de un argumento donde se debe preservar la naturaleza valor r/valor l. Nota: hay situaciones en las que el valor r/valor l no puede o no debe ser preservado, en estas situaciones std::forward no debe ser utilizado (ver el inverso a continuación).

Hay muchos ejemplos apareciendo que inadvertidamente pierden la naturaleza del valor-r/valor-l de los argumentos a través de un uso aparentemente inocente de una referencia de valor-r.

Siempre ha sido difícil escribir código genérico bien definido y sonoro. Con la introducción de las referencias de valor r, y el colapso de referencia en particular, se ha hecho posible escribir mejor código genérico, de manera más concisa, pero debemos ser cada vez más conscientes de cuál es la naturaleza original de los argumentos proporcionados y asegurarnos de que se mantienen cuando los usamos en el genérico código que escribimos.

El código de muestra completo se puede encontrar aquí

Corolario y converse

  • Un corolario de la pregunta sería: dado el colapso de referencia en una función templada, ¿cómo se mantiene la naturaleza del valor r/valor l del argumento? La respuesta - use std::forward<T>(t).
  • Converse; ¿resuelve std::forward todos sus problemas de "referencia universal"? No, no lo hace, hay casos en los que no se debe usar, como reenviar el valor más de una vez.

Breve trasfondo para perfect forwarding

El reenvío perfecto puede ser desconocido para algunos, entonces, ¿qué es el reenvío perfecto?

En resumen, perfect forwarding está ahí para asegurar que el argumento proporcionado a una función sea reenviado (pasado) a otra función con la misma categoría de valor (básicamente valor r vs.valor l) como originalmente proporcionado. Se utiliza típicamente con funciones de plantilla donde referencia el colapso puede haber tenido lugar.

Scott Meyers da el siguiente pseudo código en su presentación Going Native 2013 para explicar el funcionamiento de std::forward (aproximadamente en la marca de 20 minutos);

template <typename T>
T&& forward(T&& param) { // T&& here is formulated to disallow type deduction
  if (is_lvalue_reference<T>::value) {
    return param; // return type T&& collapses to T& in this case
  }
  else {
    return move(param);
  }
}

Perfect forwarding depende de un puñado de construcciones de lenguaje fundamentales nuevas para C++11 que forman las bases de gran parte de lo que ahora vemos en la programación genérica: {[56]]}

  • Colapso de referencia
  • Rvalue references
  • Mover semántica

El uso de std::forward está actualmente previsto en la fórmula std::forward<T>, comprender cómo funciona std::forward ayuda a comprender por qué esto es así, y también ayuda a identificar el uso no idiomático o incorrecto de rvalues, el colapso de referencias y otros.

Thomas Becker proporciona una buena, pero densa escritura sobre el perfecto reenvío problema y solución .

¿Qué son los calificadores de ref?

Los calificadores de ref (lvalue ref-qualifier & y rvalue ref-qualifier &&) son similares a los cv-qualifiers en que (los miembros ref-qualifier) se utilizan durante overload resolution para determinar qué método llamar. Se comportan como esperarías que lo hicieran; el & se aplica a lvalues y && a rvalues. Nota: A diferencia de la calificación cv, *this sigue siendo una expresión de valor l.

 46
Author: Niall,
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
2017-05-23 12:00:10

He Aquí un ejemplo práctico.

struct concat {
  std::vector<int> state;
  std::vector<int> const& operator()(int x)&{
    state.push_back(x);
    return state;
  }
  std::vector<int> operator()(int x)&&{
    state.push_back(x);
    return std::move(state);
  }
  std::vector<int> const& operator()()&{ return state; }
  std::vector<int> operator()()&&{ return std::move(state); }
};

Este objeto de función toma un x, y lo concatena a un std::vector interno. Luego devuelve std::vector.

Si se evalúa en un contexto rvalue move s a un temporal, de lo contrario devuelve un const& al vector interno.

Ahora llamamos apply:

auto result = apply( concat{}, std::make_tuple(2) );

Debido a que reenviamos cuidadosamente nuestro objeto de función, solo se asigna 1 std::vector buffer. Simplemente se mueve a result.

Sin el cuidado reenvío, terminamos creando un std::vector interno, y lo copiamos a result, luego descartamos el std::vector interno.

Debido a que el operator()&& sabe que el objeto de función debe ser tratado como un rvalue a punto de ser destruido, puede arrancar las tripas del objeto de función mientras realiza su operación. El operator()& no puede hacer esto.

El uso cuidadoso del reenvío perfecto de objetos de función permite esta optimización.

Tenga en cuenta, sin embargo, que hay muy poco uso de esta técnica "en la naturaleza" en este punto. La sobrecarga calificada de Rvalue es oscura, y hacerlo para operator() más.

Podría ver fácilmente futuras versiones de C++ usando automáticamente el estado rvalue de un lambda implícitamente move sus datos capturados por valor en ciertos contextos, sin embargo.

 13
Author: Yakk - Adam Nevraumont,
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
2017-02-24 04:01:47