有效的双向作用域枚举映射

Effective bidirectional scoped enum mapping

本文关键字:枚举 映射 作用域 有效      更新时间:2023-10-16

>具有:

enum class A : int {
FirstA,
SecondA,
InvalidB
};
enum class B : int {
FirstB,
SecondB,
InvalidB
};

如何启用这样的东西?

B b = mapper[A::FirstA];
A a = mapper[B::SecondB];

一种可能的解决方案是创建一个Mapper模板类,它允许通过构造函数中的初始值设定项列表指定映射,如下所示:

Mapper<A, B> mapper(
{
{A::FirstA,   B::SecondB},
{A::SecondA,  B::FirstB}
},
{A::InvalidA, B::InvalidB} // this is for conversions, where no mapping is specified
);

但在内部,这将需要妥协 - 要么是两张地图(从AB,从BA),要么是一张地图,例如从AB和线性搜索BA转换)。

是否可以在标准C++14中实现这一点,以便:

  • 不使用双容器
  • 查找性能在两个方向上同样出色
  • 定义和使用映射相对简单(内部实现不需要)

根据要求,:

  • 它不是身份映射(即来自A的值没有映射到相同的基础值B
  • AB的基础类型可能有所不同
  • 映射在编译时已知

您可以使用函数模板和完全专业化轻松完成此操作。 您使主模板返回无效大小写,然后专用化将返回所需的映射。

如果你有

template<A>
B mapper() { return B::InvalidB; }
template<B>
A mapper() { return A::InvalidA; }

然后,您可以添加所有映射的值,例如

template<>
B mapper<A::FirstA>() { return B::SecondB; }
template<>
B mapper<A::SecondA>() { return B::FirstB; }
template<>
A mapper<B::FirstB>() { return A::SecondA; }
template<>
A mapper<B::SecondB>() { return A::FirstA; }

然后你会这样称呼它

B b = mapper<A::FirstA>();
A a = mapper<B::SecondB>();

这使您根本没有容器。 您甚至可以制作一些宏来简化此操作,例如

#define MAKE_ENUM_MAP(from, to) 
template<from> 
auto mapper() { return to::Invalid; } 
template<to> 
auto mapper() { return from::Invalid; }

#define ADD_MAPPING(from_value, to_value) 
template<> 
auto mapper<from_value>() { return to_value; } 
template<> 
auto mapper<to_value>() { return from_value; }

然后你会像

MAKE_ENUM_MAP(A, B)
ADD_MAPPING(A::FirstA, B::SecondB)
ADD_MAPPING(A::SecondA, B::FirstB)

为您生成所有代码。 上面的版本使用单个枚举值Invalid作为映射的无效情况。 如果您不希望这样做,则可以向宏中添加用于无效映射的fromto值,例如

#define MAKE_ENUM_MAP(from, from_value, to, to_value) 
template<from> 
auto mapper() { return to_value; } 
template<to> 
auto mapper() { return from_value; }

你会这样称呼它

MAKE_ENUM_MAP(A, A::InvalidA, B, B::InvalidB)

Nathan的解决方案在实现优雅方面很难被击败。但是,如果您迫切需要一个不依赖于宏或也可以在运行时使用的解决方案,那么您可以在一个简单的对列表中指定映射。

在它的核心,我们使用两个枚举都应该具有连续的基础整数值(从零开始)的事实,这意味着我们可以将两个方向的映射表示为简单的数组。这一切都constexpr因此在编译时情况下零开销。对于运行时的使用,这确实会存储两次信息以允许即时查找,但仅占用N (sizeof(A) + sizeof(B))存储。我不知道有任何数据结构做得更好(即不会存储两个数组之一之外的任何其他数据,并且比两个方向的线性搜索更好)。请注意,这占用的存储与存储对本身相同(但不会从映射的双射性中获得任何收益)。

template<class TA, class TB, class ... Pairs>
struct Mapper
{
constexpr static std::array<TA, sizeof...(Pairs)> generateAIndices()
{
std::array<TA, sizeof...(Pairs)> ret{};
((void)((ret[static_cast<std::size_t>(Pairs::tb)] = Pairs::ta), 0), ...);
return ret;
}
constexpr static std::array<TB, sizeof...(Pairs)> generateBIndices()
{
std::array<TB, sizeof...(Pairs)> ret{};
((void)((ret[static_cast<std::size_t>(Pairs::ta)] = Pairs::tb), 0), ...);
return ret;
}
constexpr TB operator[](TA ta)
{
return toB[static_cast<std::size_t>(ta)];
}
constexpr TA operator[](TB tb)
{
return toA[static_cast<std::size_t>(tb)];
}
static constexpr std::array<TA, sizeof...(Pairs)> toA = generateAIndices();
static constexpr std::array<TB, sizeof...(Pairs)> toB = generateBIndices();
};

(这使用折叠表达式 + 逗号运算符为数组元素赋值,例如参见此处)。

用户代码提供了要使用的映射对列表,并完成:

using MyMappingList = PairList<
MyMappingPair<A::A1, B::B2>,
MyMappingPair<A::A2, B::B3>,
MyMappingPair<A::A3, B::B4>,
MyMappingPair<A::A4, B::B1>
>;
auto mapper = makeMapper<A, B>(MyMappingList{});

演示包括完整的编译时测试用例和最高效的运行时代码(字面意思只是mov)。


下面是一个仅在编译时起作用的先前版本(另请参阅修订历史记录): https://godbolt.org/z/GCkAhn

如果需要执行运行时查找,以下方法将在两个方向上处理复杂度 O(1)。

由于AB的所有枚举器均未初始化,因此第一个枚举器的值为零,第二个枚举器的值为1,依此类推。 关于这些零起始整数作为数组的索引,我们可以使用两个数组构造一个双向映射。 例如,假设当前映射为

A::FirstA  (=0) <--> B::SecondB (=1),
A::SecondA (=1) <--> B::FirstB  (=0),

,然后让我们定义以下两个数组

A arrA[2] = {A::SecondA, A::FirstA},
B arrB[2] = {B::SecondB, B::FirstB},

其中arrA[i]是对应于BiA的枚举器,反之亦然。 在此设置中,我们可以执行从A aB的查找,arrB[std::size(a)],反之亦然,复杂度为O(1)。


以下类biENumMap是上述具有 C++14 及以上的双向方法的实现示例。 请注意,由于扩展的 constexpr 从 C++14 开始可用,因此这里的 ctor 也可以是一个常量表达式。 两个重载operator()分别是来自AB的查找函数。 这些也可以是常量表达式,此类使我们能够在编译时和运行时执行双向查找:

template<std::size_t N>
class biENumMap
{
A arrA[N];
B arrB[N];
public:
constexpr biENumMap(const std::array<std::pair<A,B>, N>& init) 
: arrA(), arrB()
{        
for(std::size_t i = 0; i < N; ++i)
{
const auto& p = init[i];
arrA[static_cast<std::size_t>(p.second)] = p.first;
arrB[static_cast<std::size_t>(p.first) ] = p.second;
}
}
constexpr A operator()(B b) const{
return arrA[static_cast<std::size_t>(b)];
}
constexpr B operator()(A a) const{
return arrB[static_cast<std::size_t>(a)];
}
};

我们可以按如下方式使用此类:

演示

// compile-time construction.
constexpr biEnumMap<3> mapper({{
{A::FirstA  , B::SecondB },
{A::SecondA , B::FirstB  },
{A::InvalidA, B::InvalidB} }});
// compile-time tests, A to B.
static_assert(mapper(A::FirstA  ) == B::SecondB );
static_assert(mapper(A::SecondA ) == B::FirstB  );
static_assert(mapper(A::InvalidA) == B::InvalidB);
// compile-time tests, B to A.
static_assert(mapper(B::FirstB  ) == A::SecondA );
static_assert(mapper(B::SecondB ) == A::FirstA  );
static_assert(mapper(B::InvalidB) == A::InvalidA);
// run-time tests, A to B.
std::vector<A> vA = {A::FirstA, A::SecondA, A::InvalidA};
assert(mapper(vA[0]) == B::SecondB );
assert(mapper(vA[1]) == B::FirstB  );
assert(mapper(vA[2]) == B::InvalidB);    
// run-time tests, B to A.
std::vector<B> vB = {B::FirstB, B::SecondB, B::InvalidB};
assert(mapper(vB[0]) == A::SecondA );
assert(mapper(vB[1]) == A::FirstA  );
assert(mapper(vB[2]) == A::InvalidA);