Trabajo de tipo seguro con registros sin sobrecarga en C ++ 17: metaprogramación basada en valores

C ++, debido a su mecanografía estricta, puede ayudar al programador en la etapa de compilación. Ya hay bastantes artículos en el centro que describen cómo, usando tipos, lograr esto, y eso está bien. Pero en todo lo que leo, hay un defecto. Compare con el enfoque ++ y el enfoque C usando CMSIS, que es familiar en el mundo de la programación de microcontroladores:


some_stream.set (Direction::to_periph) SOME_STREAM->CR |= DMA_SxCR_DIR_0 .inc_memory() | DMA_SxCR_MINC_Msk .size_memory (DataSize::word16) | DMA_SxCR_MSIZE_0 .size_periph (DataSize::word16) | DMA_SxCR_PSIZE_0 .enable_transfer_complete_interrupt(); | DMA_SxCR_TCIE_Msk; 

Es obvio de inmediato que el enfoque de C ++ es más legible, y dado que cada función adquiere un tipo específico, no se puede confundir. El enfoque C no verifica la validez de los datos, recae en el programador. Como regla, un error se reconoce solo durante la depuración. Pero el enfoque de c ++ no es gratuito. De hecho, cada función tiene su propio acceso al registro, mientras que en C la máscara se recopila primero de todos los parámetros en la etapa de compilación, ya que estas son todas constantes y se escriben en el registro a la vez. A continuación, describiré cómo intenté combinar la seguridad de tipos con ++ para minimizar el acceso a mayúsculas y minúsculas. Verás que es mucho más simple de lo que parece.


Primero daré un ejemplo de cómo me gustaría que se vea. Es deseable que esto no difiera mucho del enfoque ya conocido de C ++.


 some_stream.set( dma_stream::direction::to_periph , dma_stream::inc_memory , dma_stream::memory_size::byte16 , dma_stream::periph_size::byte16 , dma_stream::transfer_complete_interrupt::enable ); 

Cada parámetro en el método set es un tipo separado por el cual puede comprender en qué registro desea escribir el valor, lo que significa que durante la compilación puede optimizar el acceso a los registros. El método es variado, por lo que puede haber cualquier número de argumentos, pero debe verificarse que todos los argumentos pertenezcan a esta periferia.


Anteriormente, esta tarea me parecía bastante complicada, hasta que encontré este video sobre metaprogramación basada en valores . Este enfoque de la metaprogramación le permite escribir algoritmos generalizados como si fuera un código normal más. En este artículo daré solo el video más necesario para resolver el problema, hay algoritmos mucho más generalizados.


Resolveré el problema en abstracto, no para una periferia específica. Entonces, hay varios campos de registro, los escribiré condicionalmente como enumeraciones.


 enum struct Enum1 { _0, _1, _2, _3 }; enum struct Enum2 { _0, _1, _2, _3 }; enum struct Enum3 { _0, _1, _2, _3, _4 }; enum struct Enum4 { _0, _1, _2, _3 }; 

Los primeros tres se referirán a una periferia, el cuarto a otro. Por lo tanto, si ingresa el valor de la cuarta enumeración en el método de la primera periferia, debe haber un error de compilación, preferiblemente comprensible. Además, los primeros 2 listados se relacionarán con un registro, el tercero con otro.


Dado que los valores de las enumeraciones no almacenan nada excepto los valores reales, se necesita un tipo adicional que almacenará, por ejemplo, una máscara para determinar en qué parte del registro se escribirá esta enumeración.


 struct Enum1_traits { static constexpr std::size_t mask = 0b00111; }; struct Enum2_traits { static constexpr std::size_t mask = 0b11000; }; struct Enum3_traits { static constexpr std::size_t mask = 0b00111; }; struct Enum4_traits { static constexpr std::size_t mask = 0b00111; }; 

Queda por conectar estos 2 tipos. Aquí el chip ya es útil para 20 estándares , pero es bastante trivial y puede implementarlo usted mismo.


 template <class T> struct type_identity { using type = T; }; //    constexpr auto some_type = type_identity<Some_type>{}; //      using some_type_t = typename decltype(some_type)::type; #define TYPE(type_identity) typename decltype(type_identity)::type 

La conclusión es que puede hacer un valor de cualquier tipo y pasarlo a la función como argumento. Este es el ladrillo principal del enfoque de metaprogramación basado en valores, en el que debe intentar pasar información de tipo a través de valores, y no como un parámetro de plantilla. Aquí definí una macro, pero soy un adversario de ellos en c ++. Pero permite más escribir menos. A continuación, daré una enumeración de enlace y sus propiedades a una función y otra macro que permite reducir el número de copias y pegados.


 constexpr auto traits(type_identity<Enum1>) { return type_identity<Enum1_traits>{}; } #define MAKE_TRAITS_WITH_MASK(enum, mask_) struct enum##_traits { \ static constexpr std::size_t mask = mask_; \ }; \ constexpr auto traits(type_identity<enum>) { \ return type_identity<enum##_traits>{}; \ } 

Los campos deben estar asociados con los registros correspondientes. Elegí la relación a través de la herencia, ya que el estándar ya tiene la std::is_base_of , que le permitirá definir la relación entre campos y registros que ya están en forma generalizada. No puede heredar de enumeraciones, por lo que heredamos de sus propiedades.


 struct Register1 : Enum1_traits, Enum2_traits { static constexpr std::size_t offset = 0x0; }; 

La dirección donde se encuentra el registro se almacena como un desplazamiento desde el comienzo de la periferia.


Antes de describir la periferia, es necesario hablar sobre la lista de tipos en la metaprogramación basada en valores. Esta es una estructura bastante simple que le permite guardar varios tipos y pasarlos por valor. Un poco como type_identity , pero para algunos tipos.


 template <class...Ts> struct type_pack{}; using empty_pack = type_pack<>; 

Puede implementar muchas funciones constexpr para esta lista. Su implementación es mucho más fácil de entender que las famosas listas de tipos Alexandrescu (biblioteca Loki). Los siguientes son ejemplos.


La segunda propiedad importante de la periferia debe ser la capacidad de ubicarla en una dirección específica (en el microcontrolador) y pasar la dirección dinámicamente para las pruebas. Por lo tanto, la estructura de la periferia será repetitiva, y como parámetro tomará un tipo que almacenará una dirección específica de la periferia en el campo de valor. El parámetro de plantilla se determinará a partir del constructor. Bueno, el método establecido, que se mencionó anteriormente.


 template<class Address> struct Periph1 { Periph1(Address) {} static constexpr auto registers = type_pack<Register1, Register2>{}; template<class...Ts> static constexpr void set(Ts...args) { ::set(registers, Address::value, args...); } }; 

Todo lo que hace el método set es llamar a una función libre, pasando toda la información necesaria para el algoritmo generalizado.


Daré ejemplos de tipos que proporcionan una dirección a la periferia.


 //    struct Address { static constexpr std::size_t value = SOME_PERIPH_BASE; }; //    ,    struct Address { static inline std::size_t value; template<class Pointer> Address(Pointer address) { value = reinterpret_cast<std::size_t>(address); } }; 

Toda la información para el algoritmo generalizado está preparada, queda por implementarlo. Daré el texto de esta función.


 template<class...Registers, class...Args> constexpr void set(type_pack<Registers...> registers, std::size_t address, Args...args) { //       ,  value based  constexpr auto args_traits = make_type_pack(traits(type_identity<Args>{})...); //              static_assert(all_of(args_traits, [](auto arg){ return (std::is_base_of_v<TYPE(arg), Registers> || ...); }), "one of arguments in set method don`t belong to periph type"); //   ,      constexpr auto registers_for_write = filter(registers, [](auto reg){ return any_of(args_traits, [](auto arg){ //       o  reg? return std::is_base_of_v<TYPE(arg), TYPE(reg)>; }); }); //           foreach(registers_for_write, [=](auto reg){ auto value = register_value(reg, args...); auto offset = decltype(reg)::type::offset; write(address + offset, value); }); }; 

Implementar una función que convierta argumentos (campos de registro específicos) en type_pack es bastante trivial. Permítame recordarle que los puntos suspensivos de la lista de tipos de plantillas revelan una lista de tipos separados por comas.


 template <class...Ts> constexpr auto make_type_pack(type_identity<Ts>...) { return type_pack<Ts...>{}; } 

Para verificar que todos los argumentos se relacionan con los registros transferidos y, por lo tanto, con periféricos específicos, es necesario implementar el algoritmo all_of. Por analogía con la biblioteca estándar, el algoritmo recibe una lista de tipos y una función de predicado como entrada. Usamos una lambda como función.


 template <class F, class...Ts> constexpr auto all_of(type_pack<Ts...>, F f) { return (f(type_identity<Ts>{}) and ...); } 

Aquí, por primera vez, se aplica una expresión de exploración de 17 estándares . Es esta innovación la que simplificó enormemente la vida de aquellos a quienes les gusta la metaprogramación. En este ejemplo, la función f se aplica a cada uno de los tipos de la lista Ts, convirtiéndola en type_identity , y el resultado de cada llamada es recopilado por I.


Dentro de static_assert , se aplica este algoritmo. args_traits envueltos en type_identity se pasa a la lambda a su vez. Dentro de la lambda, se usa la metafunción estándar std :: is_base_of, pero como puede haber más de un registro, se usa una expresión de escaneo para ejecutarlo para cada uno de los registros de acuerdo con la lógica OR. Como resultado, si hay al menos un argumento cuyas propiedades no son básicas para al menos un registro, la static assert funcionará y mostrará un mensaje de error claro. Es fácil entender desde qué lugar se encuentra el error (pasó el argumento incorrecto al método set ) y corregirlo.


La implementación del algoritmo any_of , que se necesitará más adelante, es muy similar:


 template <class F, class...Ts> constexpr auto any_of(type_pack<Ts...>, F f) { return (f(type_identity<Ts>{}) or ...); } 

La siguiente tarea del algoritmo generalizado es determinar qué registros deberán escribirse. Para hacer esto, filtre la lista inicial de registros y deje solo aquellos para los que haya argumentos en nuestra función. Necesitamos un algoritmo de filter que tome el type_pack original, aplique la función de predicado para cada tipo de la lista y lo agregue a la nueva lista si el predicado devuelve verdadero.


 template <class F, class...Ts> constexpr auto filter(type_pack<Ts...>, F f) { auto filter_one = [](auto v, auto f) { using T = typename decltype(v)::type; if constexpr (f(v)) return type_pack<T>{}; else return empty_pack{}; }; return (empty_pack{} + ... + filter_one(type_identity<Ts>{}, f)); } 

Primero, se describe una lambda que realiza la función de un predicado en un tipo y devuelve type_pack con él si el predicado devuelve verdadero, o type_pack vacío si el predicado devuelve false . Aquí ayuda otra característica nueva de las últimas ventajas: constexpr if. Su esencia es que en el código resultante solo hay uno si se bifurca, se arroja el segundo. Y dado que diferentes tipos regresan en diferentes ramas, sin constexpr habría un error de compilación. El resultado de ejecutar este lambda para cada tipo de la lista se concatena en un type_pack resultante, nuevamente gracias a la expresión de type_pack . No hay suficiente sobrecarga del operador de adición para type_pack . Su implementación también es bastante simple:


 template <class...Ts, class...Us> constexpr auto operator+ (type_pack<Ts...>, type_pack<Us...>) { return type_pack<Ts..., Us...>{}; } 

Aplicando el nuevo algoritmo sobre la lista de registros, solo aquellos en los que deberían escribirse los argumentos transferidos quedan en la nueva lista.


El siguiente algoritmo que se necesitará es foreach . Simplemente aplica una función a cada tipo en la lista, envolviéndola en type_identity . Aquí, se utiliza un operador de coma en la expresión de exploración, que realiza todas las acciones descritas por una coma y devuelve el resultado de la última acción.


 template <class F, class...Ts> constexpr void foreach(type_pack<Ts...>, F f) { (f(type_identity<Ts>{}), ...); } 

La función le permite acceder a cada uno de los registros donde desea escribir. La lambda calcula el valor para escribir en el registro, determina la dirección donde desea escribir y escribe directamente en el registro.


Para calcular el valor de un registro, se calcula el valor de cada argumento al que pertenece este registro, y el resultado se combina con OR.


 template<class Register, class...Args> constexpr std::size_t register_value(type_identity<Register> reg, Args...args) { return (arg_value(reg, args) | ...); } 

El cálculo de un valor para un campo específico debe realizarse solo para los argumentos de los que se hereda este registro. Para el argumento, extraemos una máscara de su propiedad, determinamos el desplazamiento del valor dentro del registro desde la máscara.


 template<class Register, class Arg> constexpr std::size_t arg_value(type_identity<Register>, Arg arg) { constexpr auto arg_traits = traits(type_identity<Arg>{}); //   ,     if constexpr (not std::is_base_of_v<TYPE(arg_traits), Register>) return 0; constexpr auto mask = decltype(arg_traits)::type::mask; constexpr auto arg_shift = shift(mask); return static_cast<std::size_t>(arg) << arg_shift; } 

Puede escribir el algoritmo para determinar el desplazamiento de la máscara usted mismo, pero utilicé la función incorporada existente.


 constexpr auto shift(std::size_t mask) { return __builtin_ffs(mask) - 1; } 

La última función que escribe el valor en una dirección específica permanece.


 inline void write(std::size_t address, std::size_t v) { *reinterpret_cast<std::size_t*>(address) |= v; } 

Para probar la tarea, se escribe una pequeña prueba:


 // ,    volatile std::size_t arr[3]; int main() { //     ( ) //   ,         auto address = Address{arr}; auto mock_periph = Periph1{address}; //  1      //  3       3 //  4      //     0b00011001 (25) //    0b00000100 (4) mock_periph.set(Enum1::_1, Enum2::_3, Enum3::_4); // all ok // mock_periph.set(Enum4::_0); // must be compilation error } 

Todo lo escrito aquí se combinó y compiló en Godbolt . Cualquiera allí puede experimentar con el enfoque. Se puede ver que el objetivo se cumple: no hay accesos innecesarios a la memoria. El valor que debe escribirse en los registros se calcula en la etapa de compilación:


 main: mov QWORD PTR Address::value[rip], OFFSET FLAT:arr or QWORD PTR arr[rip], 25 or QWORD PTR arr[rip+8], 4 mov eax, 0 ret 



PD:
Gracias a todos por los comentarios, gracias a ellos, modifiqué ligeramente el enfoque. Puedes ver la nueva opción aquí.


  • tipos de ayudantes eliminados * _traits, la máscara se puede guardar directamente en el listado.
     enum struct Enum1 { _0, _1, _2, _3, mask = 0b00111 }; 
  • la conexión de registro con argumentos ahora no se realiza mediante herencia, ahora es un campo de registro estático
     static constexpr auto params = type_pack<Enum1, Enum2>{}; 
  • Como la conexión ya no es a través de la herencia, tuve que escribir la función contiene:
     template <class T, class...Ts> constexpr auto contains(type_pack<Ts...>, type_identity<T> v) { return ((type_identity<Ts>{} == v) or ...); } 
  • sin tipos superfluos todas las macros desaparecieron
  • Paso argumentos al método a través de los parámetros de la plantilla para usarlos en un contexto constexpr
  • ahora en el método establecido, la lógica constexpr está claramente separada de la lógica del registro en sí
     template<auto...args> static void set() { constexpr auto values_for_write = extract(registers, args...); for (auto [value, offset] : values_for_write) { write(Address::value + offset, value); } } 
  • La función de extracción asigna en constexpr una matriz de valores para escribir en los registros. Su implementación es muy similar a la función de conjunto anterior, excepto que no escribe directamente en el registro.
  • Tuve que agregar otra metafunción que convierte type_pack en una matriz de acuerdo con la función lambda.
     template <class F, class...Ts> constexpr auto to_array(type_pack<Ts...> pack, F f) { return std::array{f(type_identity<Ts>{})...}; } 

Source: https://habr.com/ru/post/483378/


All Articles