【问题标题】:Compile-time lookup table for enum枚举的编译时查找表
【发布时间】:2015-10-27 17:07:13
【问题描述】:

我有一个枚举列表,定义如下:

enum PinEnum {
    kPinInvalid,
    kPinA0,
    kPinA1,
    kPinB0,
    kPinB1,
    kPinC0,
    kPinC1,
}

这些枚举中的每一个都需要与另外两个值相关联,即端口和引脚号。目前,我正在通过运行时函数访问这些:

GPIO_TypeDef * PinGetPort(const PinEnum pin) {
    switch (pin) {
        case kPinA0:
        case kPinA1:
            return GPIOA;
        case kPinB0:
        case kPinB1:
            return GPIOB;
        case kPinC0:
        case kPinC1:
            return GPIOC;
        default:
            return NULL;
    }
}

uint16_t PinGetPin(const PinEnum pin) {
    switch (pin) {
        case kPinA0:
        case kPinB0:
        case kPinC0:
            return GPIO_Pin_0;
        case kPinA1:
        case kPinB1:
        case kPinC1:
            return GPIO_Pin_1;
        default:
            return 0;
    }
}

特别是,我这样做是因为我不希望大型查找表在运行时占用 RAM(代码大小不是问题)。

有没有办法使用编译时查找表、constexpr 函数或模板构造来做到这一点,以便语句 PinGetPin(kPinA0)PinGetPort(kPinA0) 每个都优化为单个值而不是必须经历冗长的函数调用和案例语句?这些函数的参数将始终为 const PinEnum 类型,其值在编译时已知。

例如,一个典型的使用场景如下:

const PinEnum kPinStatus = kPinB0;

int main(int argc, char ** argv) {
    ...
    PinGetPort(kPinStatus)->BSRRH = PinGetPin(kPinStatus);
    // GPIOB->BSRRH = GPIO_Pin_0; <-- should optimize to this during compilation
    ...
}

C++11 答案很好。

虽然对于编译时查找表还有其他答案,但我没有看到直接适用于这种情况的答案。它们要么需要字符串递归,要么实际计算并存储一个查找表(如果没有其他方法,这最终可能会出现)。

【问题讨论】:

  • “它们要么需要字符串递归,要么实际计算并存储一个查找表(如果没有其他方法,这最终可能会出现)。” ....这就是为什么这是重复的。这个已经被问过了。答案不是你所希望的这一事实并不能改变它们是最好的答案这一事实,正如你刚刚承认的那样!
  • @LightnessRacesinOrbit “我想在没有字符串递归或存储(在数据中)查找表的情况下执行 X,如何?”是一个不同的问题,“我想做 X,怎么做?”因为第二个的答案并不总是回答第一个。 ;)
  • @Yakk 我大体上同意,但是当 50,000 个答案已经说过“这样做的唯一方法是使用字符串递归或存储(在数据中)查找表”时,我不认为我们需要关于该主题的另一个问题。 :)

标签: c++ c++11


【解决方案1】:

使用以枚举为参数的模板结构template specializationstd::integral_constant

#include <type_traits>

enum class pin { pin0, pin1 };

template<pin> struct lookup_port;    
template<pin> struct lookup_num;

template<> struct lookup_port<pin::pin0> 
  : std::integral_constant<int, 0> { };

template<> struct lookup_num<pin::pin0> 
  : std::integral_constant<int, 520> { };

template<> struct lookup_port<pin::pin1> 
  : std::integral_constant<int, 22> { };

template<> struct lookup_num<pin::pin1> 
  : std::integral_constant<int, 5440> { };

int main()
{
    static_assert(lookup_port<pin::pin0>::value == 0, "");
    static_assert(lookup_port<pin::pin1>::value == 22, "");

    static_assert(lookup_num<pin::pin0>::value == 520, "");
    static_assert(lookup_num<pin::pin1>::value == 5440, "");
}

在 C++14 中,您的 switch 函数可以是 constexpr,这要感谢 relaxed constexpr restrictions

【讨论】:

  • 感谢有关 C++14 的说明,如果我的编译器在这种用法中确实支持它,那可能是最好的解决方案。
  • @devtk 并不是说​​ constexpr 函数不需要在编译时进行评估。并且可以在编译时评估非constexpr 函数。 constexpr 只是表示“您可以尝试将此函数的结果用作非类型模板参数或数组大小或类似参数”。对于在编译时进行评估的高概率,将值作为模板的非类型模板参数是最好的。
【解决方案2】:

制作一张桌子:

template<class...>struct types {};
template<class lhs, class rhs>struct e{};
template<class types, class lhs>
struct find{};
template<class types, class lhs>
using find_t=typename find<types,lhs>::type;

template<class T0, class R0, class...Ts>
struct find< types<e<T0,R0>,Ts...>, T0>{
  using type=R0;
};
template<class T0, class R0, class...Ts, class lhs>
struct find< types<e<T0,R0>,Ts...>, lhs>:
  find< types<Ts...>, lhs >
{};

使用:

template<PinEnum e>
using PinEnum_t = std::integral_constant<PinEnum, e>;
template<uint16_t i>
using uint16 = std::integral_constant<uint16_t, i>;

using PinGetPin_t = types<
  e<PinEnum_t<kPinA0>, uint16<GPIOA>>,
  e<PinEnum_t<kPinA1>, uint16<GPIOA>>,
  // ...
  e<PinEnum_t<kPinC1>, uint16<GPIOC>>
>;

static_assert( find_t<PinGetPin_t, PinEnum_t<kPinA0>>{}==GPIOA, "oops");

尝试访问上述系统中的无效引脚会导致编译时错误。

live example.

我将所有内容都保存在类型和一对一地图中。也可以使用多对一映射,或者没有 PinEnum_t 包装器等。

【讨论】:

    【解决方案3】:

    我不确定PinGetPort,因为我们没有GPIO_TypeDefGPIOA 等的定义,但是,如果您只是将constexpr 放在前面,PinGetPin 应该可以正常工作它,使用 C++14 编译器并使用高级优化。喜欢:

    constexpr uint16_t PinGetPin(const PinEnum pin) {
    

    在 C++11 编译器上,您可能会遇到以下问题:

    constexpr uint16_t PinGetPin(const PinEnum pin) {
        return ((pin == kPinA0) || (pin == kPinB0)) ? GPIO_PIN_0 : 
            (((pin == kPinA1) || (pin == kPinB1)) ? GPIO_PIN_1 : 0);
    }
    

    但是,如您所见,它很快就会变得丑陋......

    【讨论】:

      【解决方案4】:

      您可以使用 C 数组来定义端口和引脚。然后使用枚举值在数组中访问它们。这使用了默认枚举值是顺序的这一事实。当然,这不是编译时间。

      【讨论】:

      • 其实没有答案(问题中提到了“计算和存储查找表”),但明智的做法是做。另请参阅 @Lightness Races in Orbit 的评论。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-05
      • 1970-01-01
      • 2020-09-27
      • 2012-10-06
      • 1970-01-01
      相关资源
      最近更新 更多