c++ - static_cast - std::< dynamic_cast
¿Por qué usar std:: forward<T> en lugar de static_cast<T &&> (3)
Cuando se le da código de la siguiente estructura
template <typename... Args>
void foo(Args&&... args) { ... }
A menudo he visto que el código de la biblioteca usa static_cast<Args&&>
dentro de la función para el reenvío de argumentos. Normalmente, la justificación para esto es que el uso de static_cast
evita una static_cast
instancias innecesaria de la plantilla.
Teniendo en cuenta las reglas de colapso y deducción de plantillas del idioma. Obtenemos un reenvío perfecto con la static_cast<Args&&>
, la prueba de esta afirmación es la siguiente (dentro de los márgenes de error, que espero que una respuesta ilumine)
- Cuando se dan las referencias de rvalue (o, para completar, no hay ninguna calificación de referencia como en este ejemplo ), esto colapsa las referencias de tal manera que el resultado es un valor de r. La regla utilizada es
&& &&
->&&
(regla1
arriba) - Cuando se dan referencias de valor de l, esto colapsa las referencias de tal manera que el resultado es un valor de l. La regla utilizada aquí es
& &&
->&
(regla2
arriba)
Esto es esencialmente obtener foo()
para reenviar los argumentos a bar()
en el ejemplo anterior. Este es el comportamiento que obtendría al usar std::forward<Args>
aquí también.
Pregunta: ¿por qué usar std::forward
en estos contextos? ¿Evitar la creación de instancias extra justifica romper la convención?
El artículo n2951 Howard Hinnant especificó 6 restricciones bajo las cuales cualquier implementación de std::forward
debería comportarse "correctamente". Éstas eran
- Debería reenviar un lvalue como un lvalue
- Debe reenviar un valor como un valor
- No debe reenviar un valor como un valor de l
- Debería reenviar las expresiones menos calificadas para cv a más expresiones calificadas para cv
- Deben reenviar expresiones de tipo derivado a un tipo de base accesible e inequívoca
- No debe reenviar conversiones de tipos arbitrarias
(1) y (2) se probó que funcionan correctamente con static_cast<Args&&>
arriba. (3) - (6) no se aplica aquí porque cuando las funciones se llaman en un contexto deducido, no puede ocurrir ninguna de estas.
Nota: Personalmente prefiero usar std::forward
, pero la justificación que tengo es simplemente que prefiero atenerme a lo convencional.
Aquí están mis 2.5, no tan técnicos. El hecho de que hoy en día std::forward
sea simplemente un simple static_cast<T&&>
no significa que mañana también se implementará exactamente de la misma manera. Creo que el comité necesitaba algo para reflejar el comportamiento deseado de lo que std::forward
logra hoy, por lo tanto, el forward
que no reenvía nada en ningún lugar surgió.
Con el comportamiento y las expectativas requeridas formalizadas bajo el paraguas de std::forward
, solo teóricamente hablando, nadie impide que un implementador futuro proporcione std::forward
como no static_cast<T&&>
sino algo específico para su propia implementación, sin tomar realmente en consideración static_cast<T&&>
porque el único hecho que importa es el uso y comportamiento correctos de std::forward
.
Scott Meyers dice que std::forward
y std::move
son principalmente por conveniencia. Incluso afirma que se puede usar std::forward
para realizar la funcionalidad de std::forward
y std::move
.
Algunos extractos de "Effective Modern C ++":
Ítem 23: Entender std :: move y std :: forward
...
La historia destd::forward
es similar a la destd::move
, pero mientras questd::move
incondicionalmente emite su argumento a unrvalue
,std::forward
solo lo hace bajo ciertas condiciones.std::forward
es un lanzamiento condicional. Solo servalue
a un valor dervalue
si el argumento se inicializó con un valor dervalue
.
...
Dado que tantostd::move
comostd::forward
reducen a los lanzamientos, la única diferencia es questd::move
siempre lanza, mientras questd::forward
solo lo hace a veces, puede preguntar si podemos prescindir destd::move
y solo usastd::forward
todas partes . Desde una perspectiva puramente técnica, la respuesta es sí:std::forward
puede hacerlo todo.std::move
no es necesario. Por supuesto, ninguna de las funciones es realmente necesaria, porque podríamos escribir repartos en todas partes, pero espero que estemos de acuerdo en que eso sería, bueno, asqueroso.
...
Las atracciones destd::move
son conveniencia, menor probabilidad de error y mayor claridad ...
Para aquellos interesados, comparación de std::forward<T>
vs static_cast<T&&>
en assembly (sin ninguna optimización) cuando se llama con rvalue
y rvalue
.
forward
expresa la intención y puede ser más seguro de usar que static_cast
: static_cast
considera la conversión, pero algunas conversiones peligrosas y supuestamente no intencionales se detectan con forward
:
struct A{
A(int);
};
template<class Arg1,class Arg2>
Arg1&& f(Arg1&& a1,Arg2&& a2){
return static_cast<Arg1&&>(a2); // typing error: a1=>a2
}
template<class Arg1,class Arg2>
Arg1&& g(Arg1&& a1,Arg2&& a2){
return forward<Arg1>(a2); // typing error: a1=>a2
}
void test(const A a,int i){
const A& x = f(a,i);//dangling reference
const A& y = g(a,i);//compilation error
}
Ejemplo de mensaje de error: enlace del explorador del compilador
Cómo se aplica esta justificación: Normalmente, la justificación para esto es que el uso de static_cast evita una creación de instancias de plantilla innecesaria.
¿Es el tiempo de compilación más problemático que la capacidad de mantenimiento del código? ¿Debería el programador perder su tiempo considerando minimizar la "creación innecesaria de instancias de plantilla" en cada línea del código?
Cuando se crea una instancia de una plantilla, su creación de instancias genera instancias de la plantilla que se utilizan en su definición y declaración. Así que, por ejemplo, si tienes una función como:
template<class T> void foo(T i){
foo_1(i),foo_2(i),foo_3(i);
}
donde foo_1
, foo_2
, foo_3
son plantillas, la foo_3
instancias de foo
causará 3 instancias. Luego recursivamente, si esas funciones causan la creación de instancias de otras 3 funciones de plantilla, podría obtener 3 * 3 = 9 instancias, por ejemplo. Por lo tanto, puede considerar esta cadena de creación de instancias como un árbol en el que la creación de una función de raíz puede generar miles de instancias como un efecto de expansión exponencial. Por otro lado, una función como forward
es una hoja en este árbol de instanciación. Por lo tanto, evitar su instanciación solo puede evitar una instanciación.
Por lo tanto, la mejor manera de evitar la explosión de creación de instancias de la plantilla es usar el polimorfismo dinámico para las clases de "raíz" y el tipo de argumento de las funciones de "raíz" y luego usar el polimorfismo estático solo para las funciones de tiempo crítico que son virtualmente superiores en este árbol de creación de instancias.
Por lo tanto, en mi opinión, usar static_cast
en su lugar para evitar las instancias es una pérdida de tiempo en comparación con el beneficio de usar un código más expresivo (y más seguro). La explosión de creación de instancias de plantillas se gestiona de manera más eficiente a nivel de arquitectura de código.