使用寄存器进行类型安全的工作,而在C ++ 17中没有开销:基于值的元编程

C ++由于其严格的类型输入,可以在编译阶段为程序员提供帮助。 该中心上已经有很多文章描述了如何使用类型来实现此目标,这很好。 但是我所读的书都有一个缺陷。 与微控制器编程领域中熟悉的++方法和使用CMSIS的C方法进行比较:


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; 

显而易见,C ++方法更具可读性,并且由于每个函数都具有一种特定的类型,因此不会出错。 C语言方法不检查数据的有效性,而是由程序员负责。 通常,仅在调试期间才识别错误。 但是c ++方法不是免费的。 实际上,每个函数都有自己的寄存器访问权限,而在C语言中,由于这些都是常量,因此在编译阶段首先从所有参数中收集掩码,然后将其立即写入寄存器。 接下来,我将描述如何在最小化案例访问的情况下将类型安全性与++相结合。 您会发现它比听起来简单得多。


首先,我将举例说明我的外观。 希望这与已经熟悉的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 ); 

set方法中的每个参数都是单独的类型,您可以通过该类型了解要在哪个寄存器中写入该值,这意味着在编译过程中可以优化对寄存器的访问。 该方法是可变参数的,因此可以有任意数量的参数,但是必须检查所有参数都属于该外围设备。


早些时候,对于我来说,这项任务似乎相当复杂,直到我观看了有关基于价值的元编程的视频 。 通过这种元编程方法,您可以像编写普通代码一样编写通用算法。 在本文中,我将仅提供解决问题的最必要视频,其中有更多通用的算法。


我将以抽象方式解决此问题,而不是针对特定外围设备。 因此,有几个寄存器字段,我将有条件地将它们写为枚举。


 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 }; 

前三个与一个外围有关,第四个与另一个外围有关。 因此,如果将第四枚举的值输入到第一外围的方法中,则应该存在编译错误,最好是可以理解的。 同样,前2个列表将与一个寄存器相关,第三个列表与另一个相关。


由于枚举的值除实际值外不存储任何内容,因此需要另外一种类型,该类型将存储例如掩码,以确定将该枚举写入寄存器的哪一部分。


 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; }; 

仍然可以连接这两种类型。 在这里,该芯片已经对20种标准有用,但它是微不足道的,您可以自己实现。


 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 

最重要的是,您可以使用任何类型的值并将其作为参数传递给函数。 这是基于值的元编程方法的主要组成部分,在这种方法中,您应该尝试将类型信息传递给值,而不是作为模板参数。 在这里,我定义了一个宏,但是在c ++中我是它们的对手。 但是他允许进一步写作。 接下来,我将为一个函数和另一个宏提供链接枚举及其属性,该宏可以减少复制粘贴的数量。


 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>{}; \ } 

必须将字段与相应的寄存器关联。 我通过继承选择关系,因为该标准已经具有元函数std::is_base_of ,它将允许您以通用形式定义字段与寄存器之间的关系。 您不能从枚举继承,因此我们从其属性继承。


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

寄存器所在的地址存储为从外围开始的偏移量。


在描述外围之前,有必要讨论基于值的元编程中的类型列表。 这是一个相当简单的结构,允许您保存几种类型并按值传递它们。 有点像type_identity ,但是有一些类型。


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

您可以为此列表实现许多constexpr函数。 与著名的Alexandrescu类型列表(Loki库)相比,它们的实现更容易理解。 以下是示例。


外设的第二个重要属性应该是将其定位在特定地址(在微控制器中)并动态传递该地址以进行测试的能力。 因此,外围设备的结构将是样板,并且将在值字段中存储外围设备的特定地址的类型作为参数。 template参数将由构造函数确定。 好了,前面提到的set方法。


 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...); } }; 

set方法所做的全部工作就是调用一个自由函数,并向其中传递通用算法所需的所有信息。


我将举例说明为外围设备提供地址的类型。


 //    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); } }; 

准备了通用算法的所有信息,仍要执行。 我将给出此功能的文字。


 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); }); }; 

实现将参数(特定的寄存器字段)转换为type_pack函数非常简单。 让我提醒您,模板类型列表的省略号显示了用逗号分隔的类型列表。


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

为了验证所有参数都与传输的寄存器有关,并因此与特定的外设有关,有必要实现all_of算法。 与标准库类似,该算法接收类型列表和谓词函数作为输入。 我们使用lambda作为函数。


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

在这里,第一次使用17标准扫描表达式 。 正是这种创新极大地简化了喜欢元编程的人们的生活。 在此示例中,函数f应用于列表Ts中的每个类型,将其转换为type_identity ,并且每个调用的结果由I收集。


static_assert内部,将应用此算法。 包装在type_identity中的type_identity传递给lambda。 在lambda内部,使用了标准的元函数std :: is_base_of,但是由于可以有多个寄存器,因此将根据OR逻辑使用扫描表达式对每个寄存器执行该表达式。 结果,如果至少有一个参数的属性对于至少一个寄存器不是基本的,则static assert将起作用并显示一条明确的错误消息。 很容易理解错误的出处(将错误的参数传递给set方法)并进行修复。


稍后将需要的any_of算法的实现非常相似:


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

通用算法的下一个任务是确定需要写入哪些寄存器。 为此,请过滤寄存器的初始列表,并仅保留函数中包含参数的那些寄存器。 我们需要一个filter算法,该算法采用原始的type_pack ,对列表中的每种类型应用谓词函数,如果谓词返回true,则将其添加到新列表中。


 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)); } 

首先,描述了一个lambda,该lambda在一种类型上执行谓词的功能,如果谓词返回true,则返回type_pack如果谓词返回false则返回空的type_pack 。 最后一个优点的另一个新功能在这里有帮助-constexpr if。 其实质是在结果代码中只有一个if分支,第二个抛出。 而且由于不同的类型在不同的分支中返回,如果没有constexpr,将出现编译错误。 再次感谢type_pack表达式,对列表中每种类型执行此lambda的结果被合并为一个结果type_packtype_pack的加法运算符没有足够的重载。 它的实现也很简单:


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

将新算法应用于寄存器列表时,新列表仅保留那些应写入传输参数的寄存器。


下一个需要的算法是foreach 。 它只是将一个函数应用于列表中的每种类型,将其包装在type_identity 。 在此,扫描表达式中使用了逗号运算符,该运算符执行逗号描述的所有动作并返回上一个动作的结果。


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

该功能使您可以访问要写入的每个寄存器。 lambda计算要写入寄存器的值,确定要写入的地址,然后直接写入寄存器。


为了计算一个寄存器的值,需要计算该寄存器所属的每个自变量的值,然后将结果通过“或”进行组合。


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

只能为继承该寄存器的参数执行特定字段值的计算。 对于自变量,我们从其属性中提取掩码,并确定寄存器中值与掩码之间的偏移量。


 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; } 

您可以自己编写用于确定遮罩偏移的算法,但是我使用了现有的内置函数。


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

保留将值写入特定地址的最后一个函数。


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

为了测试任务,编写了一个小测试:


 // ,    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 } 

这里所写的一切都被组合在一起并编成了螺栓 。 那里的任何人都可以尝试这种方法。 可以看到目标已实现:没有不必要的内存访问。 在编译阶段计算需要写入寄存器的值:


 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 



PS:
感谢大家的评论,感谢他们,我对方法进行了一些修改。 您可以在此处看到新选项


  • 删除辅助类型* _traits,可以直接在列表中保存掩码。
     enum struct Enum1 { _0, _1, _2, _3, mask = 0b00111 }; 
  • 现在不通过继承进行带有参数的寄存器连接,因为它是一个静态寄存器字段
     static constexpr auto params = type_pack<Enum1, Enum2>{}; 
  • 由于连接不再通过继承进行,因此我不得不编写contains函数:
     template <class T, class...Ts> constexpr auto contains(type_pack<Ts...>, type_identity<T> v) { return ((type_identity<Ts>{} == v) or ...); } 
  • 没有多余的类型,所有宏都消失了
  • 我通过模板参数将参数传递给方法,以在constexpr上下文中使用它们
  • 现在在set方法中,constexpr逻辑与记录本身的逻辑明显分开
     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); } } 
  • 提取函数在constexpr中分配一个值数组,以写入寄存器。 它的实现与先前的set函数非常相似,不同之处在于它不直接写入寄存器。
  • 我必须添加另一个根据lambda函数将type_pack转换为数组的元函数。
     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/zh-CN483378/


All Articles