functores c++ c++11 perfect-forwarding c++14

c++ - functores - functor prolog



¿Por qué usar un valor perfectamente reenviado(un functor)? (2)

En breve...

El TL; DR, desea conservar la categoría de valor (r-value / l-value nature) del functor porque esto puede afectar la resolución de sobrecarga , en particular los miembros calificados por ref .

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

Para centrarme en el problema de la función que se reenvía, reduje la muestra (y la hice 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é se escribió el ejemplo del código como tal. Por lo tanto, además definiremos un functor general;

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

Muestra inicial

Ejecutar algún 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 el resultado es el esperado, independientemente de si se usa un valor r Functor1() o un func valor l al realizar la llamada a apply_impl y apply_impl_2 al operador de llamada sobrecargado. Se llama tanto para r-values ​​como para l-values. Bajo C ++ 03, esto era todo lo que tenía, no podía sobrecargar los métodos miembros basados ​​en la "r-value-ness" o "l-value-ness" del objeto.

Functor1 ... 1
Functor1 ... 2
Functor1 ... 3
Functor1 ... 4

Muestras con referencial calificado

Ahora necesitamos sobrecargar al operador de llamadas para extender esto un 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 muestra;

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 y ... 6
Functor2 & ... 7
Functor2 && ... 8

Discusión

En el caso de apply_impl_2 ( id 5 y 6), la salida no es como se esperaba inicialmente. En ambos casos, se llama al operator() calificado de valor l operator() (el valor r no se llama en absoluto). Es posible que se haya esperado que dado que Functor2() , un valor r, se utiliza para llamar a apply_impl_2 se habría llamado al operator() calificado de valor r operator() . La func , como un parámetro con nombre para apply_impl_2 , es una referencia de valor r, pero como se denomina, en sí misma es un valor l. Por lo tanto, el operator()(int) const& calificado de valor l operator()(int) const& se llama operator()(int) const& se llama tanto en el caso de que el valor l func2 es el argumento y el valor r Functor2() se utiliza como argumento.

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

Conclusiones

El uso de std::forward , a través de un reenvío perfecto, garantiza que conservemos la naturaleza r-value / l-value del argumento original para func . Conserva su categoría de valor .

Es necesario, std::forward puede y debe usarse para algo más que simplemente 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 r-value / l-value no se puede o no se debe conservar, en estas situaciones no se debe usar std::forward (consulte a la inversa a continuación).

Hay muchos ejemplos emergentes que pierden inadvertidamente la naturaleza de 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 sano. Con la introducción de las referencias de valores r, y el colapso de referencias en particular, ha sido posible escribir un código genérico mejor, de forma 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 código genérico que escribimos.

Código de ejemplo completo se puede encontrar here

Corolario y converse

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

Breve fondo para el reenvío perfecto

El reenvío perfecto puede ser desconocido para algunos, entonces, ¿cuál es el reenvío perfecto ?

En resumen, el reenvío perfecto está allí para garantizar que el argumento proporcionado a una función se reenvíe (pase) a otra función con la misma categoría de valor (básicamente, valor r vs. valor l) como se proporcionó originalmente . Normalmente se usa con funciones de plantilla donde el colapso de referencia puede haber tenido lugar.

Scott Meyers da el siguiente pseudo código en su presentación de 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); } }

El reenvío perfecto depende de un puñado de construcciones de lenguaje fundamentales nuevas en C ++ 11 que forman las bases de gran parte de lo que ahora vemos en la programación genérica:

  • Colapso de referencia
  • Referencias de valor
  • Mover la semántica

El uso de std::forward está actualmente previsto en la fórmula std::forward<T> , la comprensión de cómo std::forward funciona ayuda a comprender por qué esto es así, y también ayuda a identificar el uso no incorrecto o incorrecto de los valores r, referencia el colapso y la ilk.

Thomas Becker ofrece una buena, pero densa redacción sobre el problem y la solution envío perfecto.

¿Qué son los ref-calificadores?

Los ref-calificadores (lvalue ref-qualifier & y rvalue ref-qualifier && ) son similares a los cv-calificadores en que ellos (los miembros calificados por ref ) se usan durante la resolución de sobrecarga para determinar a qué método llamar. Se comportan como esperarías; el & aplica a lvalues ​​y && a rvalues. Nota: A diferencia de cv-qualification, *this sigue siendo una expresión de valor l.

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

  • Referencias de valor r
  • Colapso de referencia
  • Reenvío perfecto
  • Mueve semántica, variadas plantillas y más.

Estaba buscando un draft anterior de la especificación C ++ 14 (ahora con texto actualizado) y el código en un ejemplo en §20.5.1, secuencias enteras en tiempo de compilación , que encontré interesante y peculiar.

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 apply_impl la función f en apply_impl , es decir, por qué std::forward<F>(f)(std::get... ?
  • ¿Por qué no simplemente aplicar la función como f(std::get... ?

Aquí hay 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 una x y la concatena a un std::vector interno. Luego devuelve ese std::vector .

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

Ahora llamamos apply :

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

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

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

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

El uso cuidadoso del reenvío perfecto de los 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 al operator() más.

Sin embargo, podría ver fácilmente las versiones futuras de C ++ utilizando automáticamente el estado de valor de un lambda para move implícitamente sus datos capturados por valor en ciertos contextos.