¿Por qué C++ no tiene reflexión?

Resuelto amit kumar asked hace 15 años • 16 respuestas

Ésta es una pregunta un tanto extraña. Mis objetivos son comprender la decisión de diseño del lenguaje e identificar las posibilidades de reflexión en C++.

  1. ¿Por qué el comité de lenguaje C++ no se propuso implementar la reflexión en el lenguaje? ¿Es la reflexión demasiado difícil en un lenguaje que no se ejecuta en una máquina virtual (como Java)?

  2. Si uno implementara la reflexión para C++, ¿cuáles serían los desafíos?

Supongo que los usos de la reflexión son bien conocidos: los editores se pueden escribir más fácilmente, el código del programa será más pequeño, se pueden generar simulacros para pruebas unitarias, etc. Pero sería fantástico si también pudieras comentar sobre los usos de la reflexión.

amit kumar avatar Dec 11 '08 19:12 amit kumar
Aceptado

Hay varios problemas con la reflexión en C++.

  • Es mucho trabajo agregar, y el comité de C++ es bastante conservador y no pierde tiempo en características nuevas radicales a menos que estén seguros de que darán sus frutos. (Se hizo una sugerencia para agregar un sistema de módulos similar a los ensamblados .NET y, si bien creo que hay un consenso general de que sería bueno tenerlo, no es su principal prioridad en este momento y se ha pospuesto hasta mucho después. C++ 0x. La motivación para esta característica es deshacerse del #includesistema, pero también permitiría al menos algunos metadatos).

  • No pagas por lo que no usas. Esa es una de las filosofías de diseño básicas que subyacen a C++. ¿Por qué mi código debería contener metadatos si es posible que nunca los necesite? Además, la adición de metadatos puede impedir que el compilador optimice. ¿Por qué debería pagar ese costo en mi código si es posible que nunca necesite esos metadatos?

  • Lo que nos lleva a otro punto importante: C++ ofrece muy pocas garantías sobre el código compilado. El compilador puede hacer prácticamente cualquier cosa que quiera, siempre que la funcionalidad resultante sea la esperada. Por ejemplo, no es necesario que tus clases estén allí . El compilador puede optimizarlos, incorporar todo lo que hacen y frecuentemente hace precisamente eso, porque incluso el código de plantilla simple tiende a crear bastantes instancias de plantilla. La biblioteca estándar de C++ se basa en esta optimización agresiva. Los funtores solo funcionan si se puede optimizar la sobrecarga de crear instancias y destruir el objeto. operator[]en un vector solo es comparable en rendimiento a la indexación de matrices sin formato porque todo el operador puede insertarse y, por lo tanto, eliminarse por completo del código compilado. C# y Java ofrecen muchas garantías sobre el resultado del compilador. Si defino una clase en C#, entonces esa clase existirá en el ensamblado resultante. Incluso si nunca lo uso. Incluso si todas las llamadas a sus funciones miembro pudieran incluirse. La clase tiene que estar ahí, para que la reflexión pueda encontrarla. Parte de esto se alivia mediante la compilación de C# en código de bytes, lo que significa que el compilador JIT puede eliminar definiciones de clases y funciones en línea si lo desea, incluso si el compilador inicial de C# no puede. En C++, solo tienes un compilador y tiene que generar código eficiente. Si se le permitiera inspeccionar los metadatos de un ejecutable de C++, esperaría ver todas las clases que define, lo que significa que el compilador tendría que conservar todas las clases definidas, incluso si no son necesarias.

  • Y luego están las plantillas. Las plantillas en C++ no se parecen en nada a las genéricas en otros lenguajes. Cada creación de instancias de plantilla crea un nuevo tipo. std::vector<int>es una clase completamente separada de std::vector<float>. Eso suma muchos tipos diferentes en un programa completo. ¿Qué debería ver nuestro reflejo? La plantilla std::vector ? Pero, ¿cómo puede ser posible, ya que se trata de una construcción de código fuente que no tiene significado en tiempo de ejecución? Tendría que ver las clases separadas std::vector<int>y std::vector<float>. Y std::vector<int>::iteratory std::vector<float>::iterator, lo mismo para const_iteratory así sucesivamente. Y una vez que ingresa a la metaprogramación de plantillas, rápidamente termina creando instancias de cientos de plantillas, todas las cuales son incorporadas y eliminadas nuevamente por el compilador. No tienen ningún significado, excepto como parte de un metaprograma en tiempo de compilación. ¿Todos estos cientos de clases deberían ser visibles para la reflexión? Tendrían que hacerlo, porque de lo contrario nuestra reflexión sería inútil, si ni siquiera garantiza que las clases que definí realmente estarán allí . Y un problema secundario es que la clase de plantilla no existe hasta que se crea una instancia. Imagine un programa que utiliza std::vector<int>. ¿Nuestro sistema de reflexión debería poder ver std::vector<int>::iterator? Por un lado, es de esperarse. Es una clase importante y se define en términos de std::vector<int>, que existe en los metadatos. Por otro lado, si el programa nunca utiliza realmente esta plantilla de clase de iterador, su tipo nunca habrá sido instanciado y, por lo tanto, el compilador no habrá generado la clase en primer lugar. Y es demasiado tarde para crearlo en tiempo de ejecución, ya que requiere acceso al código fuente.

  • Y, por último, la reflexión no es tan vital en C++ como lo es en C#. La razón es nuevamente la metaprogramación de plantillas. No puede resolverlo todo, pero en muchos casos en los que, de otro modo, recurrirías a la reflexión, es posible escribir un metaprograma que haga lo mismo en tiempo de compilación. boost::type_traitses un ejemplo sencillo. ¿ Quieres saber sobre el tipo T? Comprueba su type_traits. En C#, tendrías que buscar su tipo usando la reflexión. La reflexión aún sería útil para algunas cosas (el uso principal que puedo ver, que la metaprogramación no puede reemplazar fácilmente, es para el código de serialización generado automáticamente), pero conllevaría algunos costos significativos para C++, y simplemente no es necesario con tanta frecuencia como está en otros idiomas.

Editar: en respuesta a los comentarios:

cdleary: Sí, los símbolos de depuración hacen algo similar, ya que almacenan metadatos sobre los tipos utilizados en el ejecutable. Pero también sufren los problemas que describí. Si alguna vez ha intentado depurar una versión de lanzamiento, sabrá a qué me refiero. Hay grandes lagunas lógicas cuando creaste una clase en el código fuente, que se ha incluido en el código final. Si usaras la reflexión para algo útil, necesitarías que fuera más confiable y consistente. Tal como están las cosas, los tipos desaparecerían casi cada vez que se compila. Cambia un pequeño detalle y el compilador decide cambiar qué tipos se incluyen y cuáles no, como respuesta. ¿Cómo se puede extraer algo útil de eso, cuando ni siquiera se garantiza que los tipos más relevantes estarán representados en los metadatos? Es posible que el tipo que estaba buscando estuviera allí en la última compilación, pero ya no está. Y mañana, alguien registrará un pequeño cambio inocente en una pequeña función inocente, lo que hace que el tipo sea lo suficientemente grande como para que no quede completamente alineado, por lo que volverá a aparecer. Esto sigue siendo útil para los símbolos de depuración, pero no mucho más que eso. Odiaría intentar generar código de serialización para una clase bajo esos términos.

Evan Teran: Por supuesto que estos problemas podrían resolverse. Pero eso vuelve a mi punto número 1. Requeriría mucho trabajo y el comité de C++ tiene muchas cosas que considera más importantes. ¿El beneficio de obtener una reflexión limitada (y sería limitada) en C++ es realmente lo suficientemente grande como para justificar centrarse en eso a expensas de otras características? ¿Existe realmente un gran beneficio al agregar funciones al lenguaje central que ya se pueden realizar (en su mayoría) a través de bibliotecas y preprocesadores como los QT? Quizás, pero la necesidad es mucho menos urgente que si dichas bibliotecas no existieran. Sin embargo, para sus sugerencias específicas, creo que no permitirlo en las plantillas lo haría completamente inútil. Por ejemplo, no podrá utilizar la reflexión en la biblioteca estándar. ¿Qué tipo de reflejo no te dejaría ver un std::vector? Las plantillas son una gran parte de C++. Una característica que no funciona en plantillas es básicamente inútil.

Pero tienes razón, se podría implementar alguna forma de reflexión. Pero sería un cambio importante en el idioma. Tal como están las cosas ahora, los tipos son exclusivamente una construcción en tiempo de compilación. Existen para beneficio del compilador y nada más. Una vez compilado el código, no hay clases. Si se esfuerza, podría argumentar que las funciones todavía existen, pero en realidad, todo lo que hay es un montón de instrucciones de ensamblador de salto y muchas funciones push/pop de pila. No hay mucho que hacer al agregar dichos metadatos.

Pero como dije, hay una propuesta para cambios en el modelo de compilación, agregando módulos autónomos, almacenando metadatos para tipos seleccionados, permitiendo que otros módulos hagan referencia a ellos sin tener que meterse con #includes. Es un buen comienzo y, para ser honesto, me sorprende que el comité de estándares no haya descartado la propuesta simplemente por ser un cambio demasiado grande. Entonces, ¿tal vez en 5 o 10 años? :)

jalf avatar Dec 11 '2008 14:12 jalf

La reflexión requiere que algunos metadatos sobre los tipos se almacenen en algún lugar que pueda consultarse. Dado que C++ se compila en código de máquina nativo y sufre grandes cambios debido a la optimización, la vista de alto nivel de la aplicación prácticamente se pierde en el proceso de compilación y, en consecuencia, no será posible consultarlas en tiempo de ejecución. Java y .NET utilizan una representación de muy alto nivel en el código binario para máquinas virtuales, lo que hace posible este nivel de reflexión. Sin embargo, en algunas implementaciones de C++, hay algo llamado información de tipo de tiempo de ejecución (RTTI) que puede considerarse una versión simplificada de la reflexión.

Mehrdad Afshari avatar Dec 11 '2008 12:12 Mehrdad Afshari

No todos los idiomas deberían intentar incorporar todas las características de todos los demás idiomas.

C++ es esencialmente un ensamblador de macros muy, muy sofisticado. NO es (en el sentido tradicional) un lenguaje de alto nivel como C#, Java, Objective-C, Smalltalk, etc.

Es bueno tener diferentes herramientas para diferentes trabajos. Si solo tenemos martillos, todas las cosas parecerán clavos, etc. Tener lenguajes de script es útil para algunos trabajos, y los lenguajes OO reflexivos (Java, Obj-C, C#) son útiles para otra clase de trabajos, y súper -Los lenguajes básicos y eficientes, cercanos a la máquina, son útiles para otra clase de trabajos (C++, C, ensamblador).

C++ hace un trabajo increíble al extender la tecnología Assembler a niveles increíbles de gestión de la complejidad y abstracciones para hacer que la programación de tareas más grandes y complejas sea mucho más posible para los seres humanos. Pero no es necesariamente el lenguaje más adecuado para quienes abordan su problema desde una perspectiva estrictamente de alto nivel (Lisp, Smalltalk, Java, C#). Si necesita un lenguaje con esas características para implementar mejor una solución a sus problemas, ¡agradezca a quienes han creado dichos lenguajes para que todos los usemos!

Pero C++ es para aquellos que, por cualquier motivo, necesitan tener una fuerte correlación entre su código y el funcionamiento de la máquina subyacente. Ya sea por eficiencia, controladores de dispositivos de programación, interacción con los servicios del sistema operativo de nivel inferior o lo que sea, C++ se adapta mejor a esas tareas.

C#, Java y Objective-C requieren un sistema de ejecución mucho más grande y rico para respaldar su ejecución. Ese tiempo de ejecución debe entregarse al sistema en cuestión, preinstalado para respaldar el funcionamiento de su software. Y esa capa debe mantenerse para varios sistemas de destino, personalizada por ALGÚN OTRO IDIOMA para que funcione en esa plataforma. Y esa capa intermedia, esa capa adaptativa entre el sistema operativo host y su código, el tiempo de ejecución, casi siempre está escrita en un lenguaje como C o C++, donde la eficiencia es la número uno, donde comprender de manera predecible la interacción exacta entre el software y el hardware puede ser bueno. entendido y manipulado para obtener el máximo beneficio.

Me encanta Smalltalk, Objective-C y tener un rico sistema de ejecución con reflexión, metadatos, recolección de basura, etc. ¡Se puede escribir un código increíble para aprovechar estas funciones! Pero eso es simplemente una capa superior en la pila, una capa que debe descansar sobre capas inferiores, que en última instancia deben reposar sobre el sistema operativo y el hardware. Y siempre necesitaremos un lenguaje que sea más adecuado para construir esa capa: C++/C/Assembler.

Anexo: C++ 11/14 continúa expandiendo la capacidad de C++ para admitir abstracciones y sistemas de nivel superior. Los subprocesamientos, la sincronización, los modelos de memoria precisos y las definiciones de máquinas abstractas más precisas están permitiendo a los desarrolladores de C++ lograr muchas de las abstracciones de alto nivel sobre las que algunos de estos lenguajes exclusivos de alto nivel solían tener dominio exclusivo, sin dejar de brindar servicios cercanos. Rendimiento del metal y excelente previsibilidad (es decir, subsistemas de tiempo de ejecución mínimo). Quizás las funciones de reflexión se habiliten selectivamente en una futura revisión de C++, para aquellos que lo deseen, o quizás una biblioteca proporcione dichos servicios de tiempo de ejecución (¿tal vez haya uno ahora, o el comienzo de uno en desarrollo?).

Mordachai avatar Nov 06 '2009 17:11 Mordachai

Si realmente desea comprender las decisiones de diseño que rodean a C++, busque una copia del Manual de referencia de C++ anotado de Ellis y Stroustrup. NO está actualizado con el último estándar, pero revisa el estándar original y explica cómo funcionan las cosas y, a menudo, cómo llegaron a ese estado.

Michael Kohne avatar Dec 11 '2008 13:12 Michael Kohne