why static_cast reinterpret_cast references dynamic_cast cast c++ templates c++17 rvalue-reference forwarding

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 && && -> && (regla 1 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 & && -> & (regla 2 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

  1. Debería reenviar un lvalue como un lvalue
  2. Debe reenviar un valor como un valor
  3. No debe reenviar un valor como un valor de l
  4. Debería reenviar las expresiones menos calificadas para cv a más expresiones calificadas para cv
  5. Deben reenviar expresiones de tipo derivado a un tipo de base accesible e inequívoca
  6. 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 de std::forward es similar a la de std::move , pero mientras que std::move incondicionalmente emite su argumento a un rvalue , std::forward solo lo hace bajo ciertas condiciones. std::forward es un lanzamiento condicional. Solo se rvalue a un valor de rvalue si el argumento se inicializó con un valor de rvalue .
...
Dado que tanto std::move como std::forward reducen a los lanzamientos, la única diferencia es que std::move siempre lanza, mientras que std::forward solo lo hace a veces, puede preguntar si podemos prescindir de std::move y solo usa std::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 de std::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.