c++ reflection

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



reflection (14)

Esta es una pregunta un tanto extraña. Mis objetivos son entender 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 dirigió a 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án los desafíos?

Supongo que los usos de la reflexión son bien conocidos: los editores pueden escribirse 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 genial si también pudieras comentar sobre los usos de la reflexión.



Básicamente es porque es un "extra opcional". Muchas personas eligen C ++ sobre lenguajes como Java y C # para tener más control sobre la salida del compilador, por ejemplo, un programa más pequeño y / o más rápido.

Si elige agregar reflexión hay varias soluciones disponibles .


Hay un montón de casos para usar la reflexión en C ++ que no se pueden abordar de manera adecuada utilizando las compilaciones de tiempo de compilación como la meta-programación de plantillas.

N3340 propone punteros ricos como una forma de introducir la reflexión en C ++. Entre otras cosas, aborda el problema de no pagar por una función a menos que la use.


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

  • Es mucho trabajo agregar, y el comité de C ++ es bastante conservador, y no dedique tiempo a nuevas características radicales a menos que esté seguro de que valdrá la pena. (Se ha hecho una sugerencia para agregar un sistema de módulos similar a los ensamblajes .NET, y aunque creo que hay un consenso general de que sería bueno tenerlo, no es su máxima prioridad en este momento y se ha rechazado hasta mucho después). C ++ 0x. La motivación para esta función es deshacerse del sistema #include , pero también habilitaría al menos algunos metadatos.

  • No pagas por lo que no usas. Esa es una de las filosofías básicas de diseño que subyacen en C ++. ¿Por qué mi código debería transportar metadatos si es que nunca lo necesito? Además, la adición de metadatos puede impedir que el compilador se optimice. ¿Por qué debo 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 tiene permitido hacer casi todo lo que quiera, siempre y cuando la funcionalidad resultante sea la esperada. Por ejemplo, no se requiere que tus clases estén realmente allí . El compilador puede optimizarlos, alinear todo lo que hacen, y con frecuencia 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 funcionalizadores solo son eficaces si la sobrecarga de crear instancias y destruir el objeto se puede optimizar. operator[] en un vector solo es comparable a la indexación de matriz sin procesar en el rendimiento porque todo el operador puede estar en línea y, por lo tanto, eliminado completamente del código compilado. C # y Java ofrecen muchas garantías sobre la salida del compilador. Si defino una clase en C #, esa clase existirá en el ensamblaje resultante. Incluso si nunca lo uso. Incluso si todas las llamadas a sus funciones miembro podrían estar en línea. La clase tiene que estar allí, para que la reflexión pueda encontrarla. Parte de esto se alivia con la compilación de C # en el bytecode, lo que significa que el compilador JIT puede eliminar las definiciones de clase y las 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 definió, 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 son nada como los genéricos en otros idiomas. Cada creación de plantillas crea un nuevo tipo. std::vector<int> es una clase completamente separada de std::vector<float> . Eso se suma a muchos tipos diferentes en un programa completo. ¿Qué debería ver nuestra reflexión? La plantilla std::vector ? Pero, ¿cómo puede hacerlo, ya que es una construcción de código fuente que no tiene sentido en el tiempo de ejecución? Tendría que ver las clases separadas std::vector<int> y std::vector<float> . Y std::vector<int>::iterator y std::vector<float>::iterator const_iterator , igual para const_iterator y así sucesivamente. Y una vez que entra en la metaprogramación de plantillas, termina rápidamente creando instancias de cientos de plantillas, todas las cuales se vuelven en línea y se eliminan de nuevo por el compilador. No tienen significado, excepto como parte de un metaprograma en tiempo de compilación. ¿Deberían todos estos cientos de clases ser visibles a 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. Imagina un programa que usa std::vector<int> . ¿Debería nuestro sistema de reflexión ver std::vector<int>::iterator ? Por un lado, ciertamente lo esperarías. 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 usa esta plantilla de clase de iterador, su tipo nunca se habrá instanciado, por lo que 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 finalmente, la reflexión no es tan vital en C ++ como lo es en C #. La razón es de nuevo, metaprogramación de plantillas. No puede resolverlo todo, pero en muchos casos en los que de otro modo recurriría a la reflexión, es posible escribir un metaprograma que haga lo mismo en tiempo de compilación. boost::type_traits es un ejemplo simple. ¿Quieres saber sobre el tipo T ? Compruebe sus type_traits . En C #, tendrías que buscar alrededor de su tipo utilizando 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 no es necesario tan a menudo como sea posible. Está en otros idiomas.

Edit: 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 has intentado depurar una versión de lanzamiento, sabrás a qué me refiero. Hay grandes huecos lógicos en los que creó una clase en el código fuente, que se ha introducido en el código final. Si tuviera que utilizar la reflexión para algo útil, necesitaría que fuera más confiable y consistente. Tal como está, los tipos desaparecerían y desaparecerían casi cada vez que se compila. Cambia un pequeño detalle, y el compilador decide cambiar qué tipos se insertan y cuáles no, como respuesta. ¿Cómo extraes algo útil de eso, cuando ni siquiera tienes garantía de que los tipos más relevantes se representarán en tus metadatos? El tipo que estaba buscando puede haber estado allí en la última versión, pero ahora se ha ido. 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 se alinee completamente, por lo que volverá de nuevo. Eso sigue siendo útil para los símbolos de depuración, pero no mucho más que eso. Odiaría intentar generar un código de serialización para una clase bajo esos términos.

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

Pero tienes razón, podría implementarse algún tipo de reflexión. Pero sería un cambio importante en el lenguaje. Tal como está ahora, los tipos son exclusivamente una construcción en tiempo de compilación. Existen para el beneficio del compilador, y nada más. Una vez que el código ha sido compilado, no hay clases. Si se estira usted mismo, podría argumentar que las funciones aún existen, pero en realidad, todo lo que hay es un montón de instrucciones de ensamblador de salto y un montón de push / pop de pila. No hay mucho para seguir, al agregar tales metadatos.

Pero como dije, hay una propuesta de cambios en el modelo de compilación, agregando módulos autocontenidos, almacenando metadatos para tipos seleccionados, permitiendo que otros módulos hagan referencia a ellos sin tener que meterse con #include s. Es un buen comienzo y, para ser honesto, me sorprende que el comité estándar no se limitara a rechazar la propuesta por ser un cambio demasiado grande. ¿Tal vez en 5-10 años? :)


La razón por la que C ++ no tiene reflejo es que esto requeriría que los compiladores agreguen información de símbolos a los archivos de objetos, como qué miembros tiene un tipo de clase, información sobre los miembros, sobre las funciones y todo. Esto esencialmente haría que los archivos de inclusión sean inútiles, ya que la información enviada por las declaraciones se leería de esos archivos de objetos (los módulos entonces). En C ++, una definición de tipo puede aparecer varias veces en un programa al incluir los encabezados respectivos (siempre que todas esas definiciones sean iguales), por lo que debería decidirse dónde colocar la información sobre ese tipo, solo para nombrar una complicacion aqui La optimización agresiva realizada por un compilador de C ++, que puede optimizar docenas de instancias de plantillas de clase, es otro punto fuerte. Es posible, pero como C ++ es compatible con C, esto se convertiría en una combinación incómoda.


La reflexión para los idiomas que tienen es acerca de cuánto del código fuente está dispuesto a dejar el compilador en su código objeto para habilitar la reflexión, y cuánta maquinaria de análisis está disponible para interpretar esa información reflejada. A menos que el compilador mantenga todo el código fuente, la reflexión estará limitada en su capacidad para analizar los datos disponibles sobre el código fuente.

El compilador de C ++ no guarda nada (bueno, ignorando RTTI), por lo que no se refleja en el lenguaje. (Los compiladores de Java y C # solo mantienen la clase, los nombres de los métodos y los tipos de retorno, así que obtienes un poco de datos de reflexión, pero no puedes inspeccionar expresiones o estructuras de programas, y eso significa que incluso en esos lenguajes "habilitados para la reflexión" la información que puede obtener es bastante escasa y, en consecuencia, realmente no puede hacer mucho análisis).

Pero puede salir del lenguaje y obtener capacidades de reflexión completas. La respuesta a otra discusión de desbordamiento de pila en la reflexión en C discute esto.


La reflexión podría ser opcional, como una directiva de preprocesador. Algo como

#pragma enable reflection

De esa manera podemos tener lo mejor de ambos mundos, sin estas bibliotecas de pragma que se crearían sin reflexión (sin ningún tipo de sobrecarga como se discutió), entonces sería responsabilidad del desarrollador individual si quieren velocidad o facilidad de uso.


La reflexión requiere que algunos metadatos sobre tipos se almacenen en algún lugar donde se puedan consultar. Como C ++ se compila en el código de máquina nativo y sufre grandes cambios debido a la optimización, la vista de alto nivel de la aplicación se pierde bastante en el proceso de compilación, por lo tanto, no será posible consultarlos 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 que hacen posible este nivel de reflexión. Sin embargo, en algunas implementaciones de C ++, hay algo que se llama Información de tipo de tiempo de ejecución (RTTI) que puede considerarse una versión simplificada de la reflexión.


Reflexión en C ++, creo que es de vital importancia si C ++ se va a utilizar como un lenguaje para el acceso a bases de datos, el manejo de sesiones web / http y el desarrollo de GUI. La falta de reflexión evita los ORM (como Hibernate o LINQ), los analizadores XML y JSON que instauran clases, la serialización de datos y muchas otras funciones (donde inicialmente se deben usar datos sin tipo de letra para crear una instancia de una clase).

Se puede usar un interruptor de tiempo de compilación disponible para un desarrollador de software durante el proceso de compilación para eliminar esta preocupación de "usted paga por lo que usa".

Si un desarrollador de la empresa no necesita la reflexión para leer datos de un puerto serie, entonces no utilice el conmutador. Pero como desarrollador de bases de datos que desea seguir utilizando C ++, se me presenta de manera constante con un código horrible y difícil de mantener que mapea Datos entre miembros de datos y construcciones de bases de datos.

Ni la serialización de Boost ni ningún otro mecanismo están realmente resolviendo la reflexión (debe ser realizada por el compilador), y una vez que se haga, C ++ será nuevamente enseñado en las escuelas y utilizado en el software que se ocupa del procesamiento de datos.

Para mí este problema # 1 (y las primitivas de subprocesos naitive es el problema # 2).



Si C ++ pudiera tener:

  • datos de miembros de clase para nombres de variables, tipos de variables y el modificador const
  • Un iterador de argumentos de función (solo posición en lugar de nombre)
  • datos de miembros de clase para nombres de funciones, tipo de retorno y el modificador const
  • lista de clases padre (en el mismo orden definido)
  • datos para miembros de la plantilla y clases para padres; la plantilla expandida (lo que significa que el tipo real estaría disponible para la API de reflexión y no la ''información de plantilla de cómo llegar'')

Eso sería suficiente para crear bibliotecas muy fáciles de usar en el centro del procesamiento de datos sin tipo que prevalece en las aplicaciones web y de bases de datos de hoy en día (todos los orms, mecanismos de mensajería, analizadores xml / json, serialización de datos, etc.).

Por ejemplo, la información básica admitida por la macro Q_PROPERTY (parte de Qt Framework) http://qt.nokia.com/doc/4.5/properties.html expandida para cubrir los métodos de clase e) - sería extraordinariamente beneficiosa para C ++ y A la comunidad de software en general.

Ciertamente, la reflexión a la que me refiero no cubrirá el significado semántico ni cuestiones más complejas (como los números de línea del código fuente de los comentarios, el análisis del flujo de datos, etc.), pero tampoco creo que sean necesarios para formar parte de un estándar de idioma.


Si realmente desea entender 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 pasa por el estándar original y explica cómo funcionan las cosas y, a menudo, cómo se hicieron de esa manera.


Todos los idiomas no deben tratar de incorporar todas las características de todos los demás idiomas.

C ++ es esencialmente un ensamblador de macros muy, muy sofisticado. NO es (en un 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, todo se verá como 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 super 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 administración de 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 un lenguaje que sea el más adecuado para aquellos que están abordando su problema desde una perspectiva estrictamente de alto nivel (Lisp, Smalltalk, Java, C #). Si necesita un idioma con esas características para implementar mejor una solución a sus problemas, entonces, ¡agradezca a aquellos que han creado dichos idiomas para que todos podamos usarlos!

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

C #, Java, Objective-C requieren un sistema de tiempo de ejecución mucho más grande y rico para soportar su ejecución. Ese tiempo de ejecución debe entregarse al sistema en cuestión, preinstalado para que sea compatible con el funcionamiento de su software. Y esa capa se debe mantener para varios sistemas de destino, personalizados por ALGUNA OTRA IDIOMA para que funcione en esa plataforma. Y esa capa intermedia, esa capa adaptable entre el sistema operativo host y su código, el tiempo de ejecución, casi siempre se escribe en un lenguaje como C o C ++ donde la eficiencia es la número 1, donde comprender de manera predecible la interacción exacta entre el software y el hardware puede ser bueno Entendido, y manipulado al máximo beneficio.

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

Anexo: C ++ 11/14 continúa ampliando la capacidad de C ++ para admitir sistemas y abstracciones de nivel superior. Los subprocesos, la sincronización, los modelos de memoria precisos, 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 que algunos de estos lenguajes de alto nivel solían tener dominios exclusivos, mientras continúan brindando información cercana al Rendimiento del metal y excelente previsibilidad (es decir, subsistemas de tiempo de ejecución mínimos). Tal vez las facilidades de reflexión se habilitarán selectivamente en una futura revisión de C ++, para aquellos que lo deseen, o tal vez una biblioteca proporcionará dichos servicios de tiempo de ejecución (tal vez haya uno ahora, ¿o los comienzos de uno en impulso?).


La reflexión puede ser y ha sido implementada en c ++ antes.

No es una función nativa de c ++ porque tiene un alto costo (memoria y velocidad) que no debe configurarse de manera predeterminada por el idioma; el idioma está orientado a "máximo rendimiento por defecto".

Como no debería pagar por lo que no necesita, y como usted mismo dice que se necesita más en los editores que en otras aplicaciones, entonces debe implementarse solo donde lo necesite, y no "forzado" a todo el código ( no necesita una reflexión sobre todos los datos con los que trabajará en un editor u otra aplicación similar).