Reflexión de C ++ (Reflexión)

Biblioteca: boost.hana boost.PFR

 

https://www.cnblogs.com/zengkefu/p/6724323.html

 

Introducción a la reflexión en C ++

 

13 de abril de 2017

Detente si has escuchado esto antes. Está trabajando en un middleware de mensajería, un motor de juego, una biblioteca de interfaz de usuario o cualquier otro proyecto de software grande que tenga que lidiar con un número de objetos en constante crecimiento y cambio. Estos objetos tienen muchas cualidades diferentes, pero se pueden agrupar por su funcionalidad: se pueden enviar a través de la red o colisionar o renderizar.

Debido a que es un buen programador que cree en el principio DRY, desea escribir el código de "acción" que  hace las cosas  en estos objetos sin repetición, y conectar tipos de mensajes específicos o tipos renderizables en su canalización genérica en los lugares apropiados. Sería realmente bueno componer objetos de manera jerárquica: por ejemplo, si tuviera una clase de widget compuesta por varios Rectángulos renderizables diferentes, quiero poder generar automáticamente el código de renderizado para mi widget basado en la lógica de renderizado existente para sus formas constituyentes .

En C ++, esto es más difícil de lo que parece. Empiece por usar herencia y un tipo principal que proporciona una interfaz unificada para el código de "acción". Pero rápidamente se encuentra con problemas con la composición y el corte de la interfaz, y si la latencia o el tamaño del código son críticos para su aplicación, las funciones virtuales le darán un impacto en el rendimiento.

Escuchas que usar una herramienta de generación de código externa (como Protobuf para la codificación de mensajes, o el compilador QT Meta-Object para widgets UI) es una práctica común para este tipo de proyectos. Este enfoque puede funcionar para ciertos casos de uso, pero se siente  mal  por algunas razones: frágil, inextensible, intrusivo y redundante.

Debido a que está en contacto con las últimas y mejores características de C ++, se entera de algo llamado Conceptos que ayudará con las deficiencias de la herencia para proporcionar una interfaz genérica a tipos relacionados. Pero incluso si está trabajando con un compilador que implementa conceptos, la introducción de un concepto "serializable" o "renderizable" no resuelve algunos de sus problemas fundamentales. Cuando desea componer objetos jerárquicamente, todavía no tiene una buena forma de detectar automáticamente cuando alguno de los miembros de su tipo también se ajusta a ese Concepto.

Este no es un problema nuevo. Vea este extracto de  The Art of Unix Programming por Eric S. Raymond , alrededor de 2003, que describe este problema tal como se manifestó en el  fetchmailconf programa:

El autor consideró escribir una capa adhesiva que supiera explícitamente sobre la estructura de las tres clases y usaría ese conocimiento para arrastrarse por el inicializador creando objetos coincidentes, pero rechazó esa idea porque era probable que se agregaran nuevos miembros de la clase con el tiempo como lenguaje de configuración. creció nuevas características. Si el código de creación de objetos se escribiera de la manera obvia, sería frágil y tendería a perder la sincronización cuando cambiaran las definiciones de clase. Una vez más, una receta para errores sin fin.

La mejor manera sería la programación basada en datos: código que analizaría la forma y los miembros del inicializador, consultaría las definiciones de clase sobre sus miembros y luego emparejaría la impedancia de los dos conjuntos.

Los programadores Lisp llaman a esto introspección; en los lenguajes orientados a objetos se llama pirateo de metaclase y generalmente se considera  magia negra profunda y terriblemente esotérica.

Hay otro nombre para este arte arcano que usaremos en este artículo: reflexión. Probablemente piense que ya hay demasiada magia negra en C ++. Pero quiero convencerlos de que hoy necesitamos la reflexión en el lenguaje, especialmente la introspección en tiempo de compilación, y que no es un misterio esotérico y temible, sino una herramienta útil que puede hacer que su base de código sea más limpia y efectiva.

La historia hasta ahora

Antes de profundizar demasiado, definamos algunos términos. Como sabemos por mi publicación anterior en el blog, los nombres son frustrantemente difíciles de precisar, así que no discutamos demasiado sobre las definiciones para que podamos llegar al contenido real.

La introspección  es la capacidad de inspeccionar un tipo y recuperar sus diversas cualidades. Es posible que desee realizar una introspección de los miembros de datos de un objeto, las funciones de los miembros, la jerarquía de herencia, etc. Y es posible que desee realizar una introspección de diferentes cosas en tiempo de compilación y tiempo de ejecución.

Los metaobjetos  son el resultado de la introspección en un tipo: un identificador que contiene los metadatos que solicitó de la introspección. Si la implementación de la reflexión es buena, este identificador de metaobjetos debe ser ligero o de coste cero en tiempo de ejecución.

Reificación  es una palabra elegante para "hacer de algo un ciudadano de primera clase" o "hacer algo concreto". Lo usaremos para referirnos al mapeo de la representación reflejada de objetos (metaobjetos) a tipos o identificadores concretos.

En realidad, hay algunas formas de lograr la reflexión en el lenguaje actual, ya sea que esté usando C ++ 98 o C ++ 14. Estos métodos tienen sus ventajas y desventajas, pero el problema fundamental en general es que no tenemos instalaciones de reflexión estandarizadas a nivel de lenguaje que resuelvan el caso de uso descrito en la introducción.

RTTI

La información / identificación del tipo en tiempo de ejecución es una característica controvertida de C ++, comúnmente vilipendiada por los maníacos del rendimiento y los fanáticos de cero gastos generales. Si alguna vez has usado  dynamic_casttypeidtype_info, que estaba usando RTTI.

RTTI es el mecanismo integrado de C ++ para hacer coincidir las relaciones entre tipos en tiempo de ejecución. Esa relación puede ser de igualdad o una relación por herencia. Por lo tanto, implementa una especie de introspección en tiempo de ejecución limitada. C ++ RTTI requiere generar un estado de tiempo de ejecución adicional para cada clase que usa vtables (tablas de funciones virtuales), la matriz de punteros que se utilizan para enviar funciones virtuales a sus implementaciones determinadas por el tiempo de ejecución. La compilación con  -fnortti es una optimización común para deshabilitar la generación de este estado adicional, que puede ahorrar tanto en el tamaño del código como en los tiempos de compilación (ya que el compilador tiene menos trabajo que hacer). Pero también desactiva la forma "idiomática" de C ++ de lograr polimorfismo en tiempo de ejecución.

Un poco de barra lateral: ¿por qué RTTI es idiomático C ++ cuando hay mejores alternativas? LLVM implementa una alternativa a RTTI que funciona en clases sin vtables, pero requiere un poco más de texto estándar (consulte el Manual del programador de LLVM ). Y, si entrecierra los ojos ante el texto estándar adicional que se usa en esta técnica, puede imaginarse formalizar esto con una biblioteca genérica e incluso agregar más ideas como conceptos dinámicos (a veces llamados interfaces en otros lenguajes). De hecho, Louis Dionne está trabajando en una  biblioteca experimental que hace precisamente eso.

Como mecanismo de reflexión, RTTI no tiene muchas ventajas, además del hecho de que está estandarizado y disponible en cualquiera de los tres compiladores principales (Clang, gcc, MSVC). No es tan poderoso como los otros mecanismos de reflexión que describimos y no se ajusta al planteamiento del problema que hicimos anteriormente. Aún así, el problema general de mapear desde la información en tiempo de ejecución a los tipos en tiempo de compilación es importante para muchas aplicaciones interesantes.

Macros

Amamos y odiamos C ++ porque el lenguaje nos permite hacer  casi cualquier cosa . Las macros son la herramienta más flexible del arsenal de pistolas de C ++.

Hay muchas cosas que puede hacer con las macros que en su lugar podría hacer con las plantillas, lo que le otorga seguridad de escritura, un mejor manejo de errores y, a menudo, mejores optimizaciones que las macros. Sin embargo, la reflexión no se puede lograr solo con plantillas y debe aprovechar las macros (hasta C ++ 17, como veremos en la siguiente sección).

Las bibliotecas de Boost C ++ ofrecen algunas bibliotecas de utilidades para la metaprogramación de plantillas, incluidas MPL, Fusion y Hana. Fusion y Hana ofrecen macros que pueden adaptar un tipo de POD definido por el usuario en una estructura introspectible que tiene un acceso similar a una tupla. Me centraré en Hana porque estoy más familiarizado con la biblioteca y es una biblioteca más nueva. Sin embargo, la macro que ofrece Fusion tiene la misma interfaz.

La documentación de Hana  ofrece una descripción general completa de estas macros. Puede definir una estructura introspectible con  BOOST_HANA_DEFINE_STRUCT:

struct Person {
  BOOST_HANA_DEFINE_STRUCT(Person,
    (std::string, name),
    (std::string, last_name),
    (int, age)
  );
};

O, si desea introspectar un tipo definido por otra biblioteca, puede usar  BOOST_HANA_ADAPT_STRUCT:

BOOST_HANA_ADAPT_STRUCT(Person, name, last_name, age);

Después de hacer esto, puede acceder a la estructura de forma genérica. Por ejemplo, puede iterar sobre los campos de la estructura usando  hana::for_each:

Person jackie{"Jackie", "Kay", 24};
hana::for_each(jackie, [](auto pair) {
  std::cout << hana::to<char const*>(hana::first(pair)) << ": "
            << hana::second(pair) << std::endl;
});
// name: Jackie
// last_name: Kay
// age: 24

La idea básica detrás de la magia es que la macro genera un conjunto genérico y funciones de obtención para cada miembro. "Cadena" el nombre de campo dado a la macro para que pueda usarse como una clave de cadena constexpr, y usa el nombre y el tipo de identificador para obtener el puntero de miembro para cada campo. Luego, adapta la estructura a una tupla de claves de cadena en tiempo de compilación, pares de accesor de puntero de miembro.

Aunque suena bastante sencillo, el código es bastante formidable y detallado. Eso es porque hay un caso de macro independiente para adaptar una estructura de tamaños específicos. Por ejemplo, todas las estructuras con 1 miembro se asignan a una macro en particular, todas las estructuras con 2 miembros se asignan a la siguiente macro, etc.

En realidad, la macro se genera a partir de una plantilla Ruby de menos de 200 líneas  . Puede que no conozcas a Ruby, pero puedes probar el patrón mirando la plantilla, que se puede expandir hasta un máximo de n = 62 (los saltos de línea son míos):

#define BOOST_HANA_DEFINE_STRUCT(...) \
    BOOST_HANA_DEFINE_STRUCT_IMPL(BOOST_HANA_PP_NARG(__VA_ARGS__), __VA_ARGS__)

#define BOOST_HANA_DEFINE_STRUCT_IMPL(N, ...) \
    BOOST_HANA_PP_CONCAT(BOOST_HANA_DEFINE_STRUCT_IMPL_, N)(__VA_ARGS__)

<% (0..MAX_NUMBER_OF_MEMBERS).each do |n| %>
#define BOOST_HANA_DEFINE_STRUCT_IMPL_<%= n+1 %>(TYPE <%= (1..n).map { |i| ", m#{i}" }.join %>) \
  <%= (1..n).map { |i| "BOOST_HANA_PP_DROP_BACK m#{i} BOOST_HANA_PP_BACK m#{i};" }.join(' ') %> \
                                                                                                \
  struct hana_accessors_impl {                                                                  \
    static constexpr auto apply() {                                                             \
      struct member_names {                                                                     \
        static constexpr auto get() {                                                           \
            return ::boost::hana::make_tuple(                                                   \
              <%= (1..n).map { |i|                                                              \
                "BOOST_HANA_PP_STRINGIZE(BOOST_HANA_PP_BACK m#{i})" }.join(', ') %>             \
            );                                                                                  \
        }                                                                                       \
      };                                                                                        \
      return ::boost::hana::make_tuple(                                                         \
        <%= (1..n).map { |i| "::boost::hana::make_pair(                                         \
            ::boost::hana::struct_detail::prepare_member_name<#{i-1}, member_names>(),          \
            ::boost::hana::struct_detail::member_ptr<                                           \
                decltype(&TYPE::BOOST_HANA_PP_BACK m#{i}),                                      \
                &TYPE::BOOST_HANA_PP_BACK m#{i}>{})" }.join(', ') %>                            \
      );                                                                                        \
    }                                                                                           \
  }                                                                                             \

¿Por qué los autores de estas bibliotecas escribieron estas macros en primer lugar? ¡Derechos de fanfarronear, por supuesto!

En realidad, el acceso tipo tupla para estructuras es increíblemente útil para la programación genérica. Le permite realizar operaciones en cada miembro de un tipo, o un subconjunto de miembros conforme a un concepto particular, sin tener que conocer los nombres específicos de cada miembro. Nuevamente, esta es una característica que no se puede obtener solo con herencia, plantillas y conceptos.

La principal desventaja de estas macros es que para reflexionar sobre un tipo es necesario definirlo con DEFINE_STRUCT, o hacer referencia a él y a todos sus miembros introspectibles en el orden correcto con ADAPT_STRUCT. Aunque la solución requiere menos redundancia lógica y de código que las técnicas de generación de código, este último fragmento de pirateo manual no sería necesario si tuviéramos una reflexión a nivel de lenguaje.

magic_get

El pináculo actual de la introspección POD en C ++ 14 es una biblioteca llamada  magic_get por Antony Polukhin. A diferencia del enfoque de Fusion o Hana,  magic_get no requiere llamar a una  macro DEFINE_STRUCT o  ADAPT_STRUCTpara cada estructura que queremos introspectar. En su lugar, puede poner un objeto de cualquier tipo de POD en las magic_get funciones de utilidad y acceder a él como una tupla. Por ejemplo:

Person author{"Jackie", "Kay", 24};
std::cout << "People are made up of " << boost::pfr::tuple_size<Person>::value << " things.\n"
std::cout << boost::pfr::get<0>(author) << " has a lot to say about reflection\n"!
// People are made up of 3 things.
// Jackie has a lot to say about reflection!

Otro aspecto interesante  magic_get es que proporciona una implementación de C ++ 17 que no utiliza macros, solo plantillas y la nueva función de enlaces estructurados.

Echemos  un vistazo debajo del capó  para ver cómo Antony logra esto:

template <class T>
constexpr auto as_tuple_impl(T&& /*val*/, size_t_<0>) noexcept {
  return sequence_tuple::tuple<>{};
}


template <class T>
constexpr auto as_tuple_impl(T&& val, size_t_<1>) noexcept {
  auto& [a] = std::forward<T>(val);
  return ::boost::pfr::detail::make_tuple_of_references(a);
}

template <class T>
constexpr auto as_tuple_impl(T&& val, size_t_<2>) noexcept {
  auto& [a,b] = std::forward<T>(val);
  return ::boost::pfr::detail::make_tuple_of_references(a,b);
}

Estas especializaciones  as_tuple_impl repiten este mismo patrón para un total de 101 especializaciones (se tomó cierta libertad con los saltos de línea):

template <class T>
constexpr auto as_tuple_impl(T&& val, size_t_<100>) noexcept {
  auto& [
    a,b,c,d,e,f,g,h,j,k,l,m,n,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,J,K,L,M,N,
    P,Q,R,S,T,U,V,W,X,Y,Z,
    aa,ab,ac,ad,ae,af,ag,ah,aj,ak,al,am,an,ap,aq,ar,as,at,au,av,aw,ax,ay,az,
    aA,aB,aC,aD,aE,aF,aG,aH,aJ,aK,aL,aM,aN,aP,aQ,aR,aS,aT,aU,aV,aW,aX,aY,aZ,
    ba,bb,bc,bd
  ] = std::forward<T>(val);

  return ::boost::pfr::detail::make_tuple_of_references(
    a,b,c,d,e,f,g,h,j,k,l,m,n,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,J,K,L,M,N,
    P,Q,R,S,T,U,V,W,X,Y,Z,
    aa,ab,ac,ad,ae,af,ag,ah,aj,ak,al,am,an,ap,aq,ar,as,at,au,av,aw,ax,ay,az,aA,
    aB,aC,aD,aE,aF,aG,aH,aJ,aK,aL,aM,aN,aP,aQ,aR,aS,aT,aU,aV,aW,aX,aY,aZ,
    ba,bb,bc,bd
  );
}

¿Que está pasando aqui? Antony está descomponiendo una estructura plana de tipo T en un montón de referencias con nombres de letras consecutivas en el alfabeto. Luego, reenvía esos campos a una tupla de referencias de su tipo de tupla personalizado. Este es el mecanismo central para "convertir" de una estructura a una tupla. Pero dado que no hay forma de desestructurar un número arbitrario de campos (¿"enlaces estructurados variadic"?), Este código no se puede escribir de forma genérica y la biblioteca, en cambio, recurre a generar estas especializaciones a través de un  script de Python .

Nuevamente, si tuviéramos una reflexión a nivel de lenguaje en C ++, estas técnicas de generación de código no serían necesarias.

Herramientas del compilador

El compilador de Clang C ++ no es solo un ejecutable independiente que toma su código fuente y escupe un ejecutable. Puede usar  libclang o  LibTooling escribir sus propias herramientas usando una interfaz para el código AST analizado de C ++. El código que comprende su propio AST no solo usa la reflexión, sino que accede a la forma más poderosa de introspección, ya que la API de introspección puede usar toda la información utilizada por el compilador.

siplasplas es un motor de reflexión escrito por Manu Sánchez que usa libclang para generar encabezados de metaobjetos para tipos C ++, permitiendo la reflexión estática y dinámica.

La desventaja de este enfoque es que está bloqueado por la API de un compilador en particular. Incluso si quisiera crear una capa de compatibilidad para las herramientas de otro compilador ... probablemente no pueda, ya que GCC y MSVC no ofrecen una biblioteca modular análoga para el acceso AST como lo hace Clang.

Reflexión en otros idiomas

Aunque los programadores de C ++ a veces desprecian a otras comunidades de programación, creo que es importante que entendamos cómo se hacen las cosas en otros lenguajes antes de emitir un juicio sobre las características del lenguaje. Esto se aplica a todos, desde los diseñadores de lenguajes que forman parte del comité que intentan mejorar el lenguaje, hasta los programadores cotidianos que podrían optar por especializarse en un lenguaje diferente y salir de la comunidad si su comparación los lleva a otra parte.

La cita anterior de Eric S. Raymond menciona la introspección en Lisp. Continúa comparando otros lenguajes que son comunes en el mundo Unix:

La mayoría de los lenguajes orientados a objetos no lo admiten [reflexión] en absoluto; en los que lo hacen (siendo Perl uno), tiende a ser una empresa complicada y frágil. Las instalaciones de Python para la piratería de metaclase son inusualmente accesibles ".

Sin embargo, The Art of Unix Programming está mostrando su edad, ya que varios otros lenguajes de programación de uso general convencionales ofrecen facilidades de reflexión con varios grados de potencia.

Pitón

Como dice ESR, la interfaz de reflexión de Python es fácil de usar y flexible, pero esto tiene un costo de rendimiento. Hay una serie de funciones de utilidad para consultar los metadatos de un objeto, como  type, hasattry el dir comando muy poderoso  , que enumera todos los nombres en el alcance actual si no se le dan argumentos, o si se le da el nombre de un objeto, devuelve una lista de los atributos del objeto. . Incluso puede usar  diren módulos para ver qué funciones y objetos importa un determinado módulo. Puede usar  setattr para agregar nuevos atributos a un objeto o  delattr para eliminar atributos.

Una de mis bibliotecas de Python favoritas es  Beautiful Soup . Es un analizador HTML / XML que hace un uso intensivo de las funciones de reflexión de Python. Cuando analiza un documento con Beautiful Soup, las etiquetas XML y los atributos de los objetos resultantes se traducen directamente a los atributos de Python. Por ejemplo, un árbol XML que tiene este aspecto:

<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>

... se puede acceder de esta manera si el documento se analiza en un objeto llamado  soup:

soup.title.string
# u'The Dormouse's story'

soup.title.parent.name
# u'head'

Este acceso funcionará para etiquetas XML arbitrarias (siempre que el XML esté bien formado).

Implementar una biblioteca de este tipo en C ++ hoy en día es básicamente imposible, porque implica traducir los valores de tiempo de ejecución en identificadores de miembros. Si aún no está convencido de que la reflexión sería excelente para las bibliotecas de analizadores, considere que la biblioteca de analizadores más poderosa en C ++ hoy en día es Boost Spirit, que hace un uso intensivo de la macro de reflexión de Boost Fusion.

Java

La API de reflexión de Java está muy estructurada (a diferencia de Python) y bastante extensa. Ofrece un conjunto de definiciones para clases que representan construcciones del lenguaje y funciones para extraer información sobre esas construcciones del lenguaje.

Aquí hay un ejemplo que crea objetos Class a partir de una lista de nombres, adaptada de  este tutorial :

public static void main(String... args) {
    Class<?> c = Class.forName(args[0]);
    out.format("Class:%n  %s%n%n", c.getCanonicalName());

    Package p = c.getPackage();
    out.format("Package:%n  %s%n%n",
         (p != null ? p.getName() : "-- No Package --"));

    for (int i = 1; i < args.length; i++) {
        switch (ClassMember.valueOf(args[i])) {
        case CONSTRUCTOR:
            printMembers(c.getConstructors(), "Constructor");
            break;
        case FIELD:
            printMembers(c.getFields(), "Fields");
            break;
        case METHOD:
            printMembers(c.getMethods(), "Methods");
            break;
        case CLASS:
            printClasses(c);
            break;
        case ALL:
            printMembers(c.getConstructors(), "Constuctors");
            printMembers(c.getFields(), "Fields");
            printMembers(c.getMethods(), "Methods");
            printClasses(c);
            break;
        default:
            assert false;
        }
    }
}

La reflexión de Java incurre en un costo de tiempo de ejecución porque, como puede ver, los objetos que resultan de la reflexión son construcciones simples de tiempo de ejecución. Esto es inaceptable para muchos de nosotros, fanáticos del desempeño en la comunidad C ++: para los casos en los que los datos de reflexión no son necesarios en tiempo de ejecución, esta no es una abstracción de costo cero.

Menciones honoríficas

El sistema de reflexión de C # es algo similar al de Java: permite la búsqueda de tipos mediante identificadores de cadena y reifica tipos como objetos de tiempo de ejecución. Tiene utilidades para obtener y configurar campos reflejados, e incluso invocar funciones reflejadas. Fue diseñado teniendo en cuenta el caso de uso de "ensamblados en tiempo de ejecución"; es decir, la capacidad de cargar bibliotecas compartidas dinámicas en tiempo de ejecución y utilizar la interfaz de reflexión para acceder a la API de la biblioteca compartida. Para más información recomiendo  estos  artículos .

Go fue diseñado para ser un reemplazo de C ++ más simple y “productivo”. Tiene algunas ventajas sobre C ++, como una biblioteca estándar más grande, pero sus facilidades para genéricos y metaprogramación están muy lejos del C ++ moderno. Este artículo (ahora algo anticuado) sobre  por qué Go no necesita genéricos  puede enojarte si te encanta la programación genérica (como yo), pero también brinda una introducción a las interfaces y la reflexión, los sustitutos de los genéricos a nivel de lenguaje. El reflect paquete Go  implementa la reflexión en tiempo de ejecución. Es esencialmente una utilidad para recuperar un identificador de tiempo de ejecución para el tipo concreto almacenado en una interfaz. Si tiene curiosidad sobre esto, lea sobre las leyes de reflexión de Go  .

Rust es un competidor notable de C ++ en muchas áreas. Al igual que C ++, actualmente carece de soporte de reflexión a nivel de lenguaje, pero es posible piratear una implementación utilizando alguna combinación de complementos, atributos y macros de AST.

El camino hacia la estandarización: reflexpr y operator $

El Comité de Normas ISO C ++ tiene un grupo de estudio sobre reflexión y metaprogramación, SG7, que está evaluando múltiples propuestas de reflexión dirigidas a C ++ 20.

La  reflexpr propuesta, de Matúš Chochlík, Axel Naumann y David Sankel, introduce varios “metaobjetos” a los que se accede pasando un tipo al nuevo  reflexpr operador. Recomiendo  "Reflexión estática en pocas palabras"  para una breve descripción del diseño y  Reflexión estática  para una descripción más amplia. Puede encontrar instrucciones para construir la bifurcación de Clang de Matúš que implementa  reflexpr, leer su documentación y explorar su  mirror biblioteca de utilidades  aquí .

Andrew Sutton y Herb Sutter escribieron  "Un diseño para la reflexión estática" , que presenta el operador de reflexión  $, como una forma de obtener metadatos de objetos de una clase, espacio de nombres, etc. ( $ se ha argumentado que el uso no  está a favor porque es común en código heredado, particularmente en la generación de código y sistemas de plantillas que no son necesariamente C ++ válido pero producen fuentes C ++). Puede explorar la bifurcación Clang de Andrew Sutton implementando la propuesta  aquí .

La diferencia de diseño fundamental entre estos dos papeles es si el resultado del operador de reflexión es un valor o un tipo.

Por ejemplo, aquí hay un ejemplo de reflexión simple con el  reflexpr papel:

namespace meta = std::meta;
using MetaPerson = reflexpr(Person);
std::cout << "A " << meta::get_base_name_v<MetaPerson> << " is made up of "
          << meta::get_size<MetaPerson> << " things.\n";
// A Person is made up of 3 things.

Y así es como harías lo mismo usando  operator$:

namespace meta = cpp3k::meta::v1;
constexpr auto info = $Person;
std::cout << "A " << info.name() << " is made up of
          " << info.member_variables().size() << " things.\n";

La discusión completa detrás de la metaprogramación basada en tipos frente a la metaprogramación basada en valores podría ser otra publicación larga de blog en sí misma. De hecho, la reflexión es un caso de uso para las instalaciones de metaprogramación, y el deseo de estandarizar la reflexión en el lenguaje está haciendo que los problemas de diseño detrás de la metaprogramación de C ++ sean más evidentes. En el estado actual de la metaprogramación de C ++, a menudo es más rápido para el compilador calcular operaciones en tipos puros que calcular operaciones en valores constexpr. Consulte el trabajo y las publicaciones del blog de Odin Holmes y los  puntos de referencia. comparando las bibliotecas MPL de Metal, Hana y Kvasir. Sin embargo, la sintaxis de la metaprogramación basada en valores, como Boost Hana, es más intuitiva y concisa que trabajar enteramente dentro de paréntesis angulares de plantilla y usar declaraciones. También es más fácil pasar del tiempo de compilación a los valores de tiempo de ejecución en el estilo de metaprogramación basado en valores.

En mi opinión, el diseño del lenguaje debería ir en la dirección de la metaprogramación basada en valores y los implementadores del compilador deberían evaluar cómo se pueden mejorar las velocidades de compilación de las bases de código con mucho constexpr. Esto haría que la metaprogramación sea mucho más accesible y práctica para el usuario medio. Sin embargo, el estándar no puede hacer cumplir los requisitos sobre tiempos de compilación; esto es completamente una cuestión de calidad de implementación, y no sé lo suficiente sobre compiladores para evaluar cuánto se podrían mejorar los tiempos de compilación, pero sí sé que los escritores de compiladores son algunos de los programadores más orientados al rendimiento del mundo.

Tenga en cuenta también que la API de reflexión basada en tipos podría integrarse en una utilidad de metaprogramación basada en valores, un punto que se destaca explícitamente en el documento "Reflexión estática en pocas palabras". Dado el estado actual de la especificación del lenguaje y las implementaciones del compilador, parece un enfoque seguro proporcionar una facilidad mínima de reflexión basada en tipos, para aquellos que quieren permanecer en la tierra de los tipos puros, y permitir a aquellos que prefieren la conveniencia del valor. -metaprogramación basada en el uso de una biblioteca de utilidades. Sin embargo, podría argumentar que este enfoque no está preparado para el futuro si el lenguaje se está alejando rápidamente de la metaprogramación de plantillas de estilo antiguo. Estos puntos son difíciles de debatir sin ejemplos concretos y experiencia de desarrollador.

Independientemente de si prefiere la metaprogramación basada en tipos o la metaprogramación basada en valores, hay algunas características de metaprogramación que deben agregarse al estándar C ++ para hacer que la reflexión estática sea más aceptable. Se necesita una cadena constexpr de representación estandarizada para los cálculos que utilizan los nombres de campos y tipos. C ++ 17 nos da if constexpr y expresiones de plegado con operadores binarios, pero expandir el flujo de control de constexpr a bucles for constexpr y utilidades de plegado más expresivas y genéricas sería extremadamente útil para el tipo de trabajo que queremos hacer con la reflexión estática. Y los conceptos son un requisito previo de diseño para los dos trabajos de reflexión, a pesar de estar en un TS y aún no ser un ciudadano de primera clase del idioma. Si algunos de estos puntos aún no le quedan del todo claros, intentaremos demostrarlos con un ejemplo en el próximo artículo de esta serie.

Conclusión (por ahora)

Creo que la reflexión es una herramienta indispensable para bibliotecas genéricas y grandes marcos que tratan con muchos tipos diferentes y relaciones complejas entre esos tipos. Estandarizar la reflexión en C ++ ayudará a que esta característica sea más accesible para el programador promedio y hará que los programadores de C ++ sean más productivos en general al fortalecer el ecosistema de bibliotecas genéricas. Proporcionar una función de reflexión sin sobrecarga también ayudará a C ++ a mejorar otros lenguajes de programación convencionales de propósito general que ofrecen reflexión, como Java, y se distingue de los lenguajes de la competencia sin funciones de reflexión, como Rust.

¡Sintonice la próxima vez para la parte 2 de esta serie, donde demostraré lo que puede hacer con la reflexión en C ++!

Supongo que te gusta

Origin blog.csdn.net/gouguofei/article/details/109199170
Recomendado
Clasificación