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