有效的双向作用域枚举映射
Effective bidirectional scoped enum mapping
>具有:
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
);
但在内部,这将需要妥协 - 要么是两张地图(从A
到B
,从B
到A
),要么是一张地图,例如从A
到B
和线性搜索B
到A
转换)。
是否可以在标准C++14
中实现这一点,以便:
- 不使用双容器
- 查找性能在两个方向上同样出色
- 定义和使用映射相对简单(内部实现不需要)
根据要求,:
- 它不是身份映射(即来自
A
的值没有映射到相同的基础值B
A
和B
的基础类型可能有所不同- 映射在编译时已知
?
您可以使用函数模板和完全专业化轻松完成此操作。 您使主模板返回无效大小写,然后专用化将返回所需的映射。
如果你有
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
作为映射的无效情况。 如果您不希望这样做,则可以向宏中添加用于无效映射的from
和to
值,例如
#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)。
由于A
和B
的所有枚举器均未初始化,因此第一个枚举器的值为零,第二个枚举器的值为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]
是对应于B
i
的A
的枚举器,反之亦然。 在此设置中,我们可以执行从A a
到B
的查找,arrB[std::size(a)]
,反之亦然,复杂度为O(1)。
以下类biENumMap
是上述具有 C++14 及以上的双向方法的实现示例。 请注意,由于扩展的 constexpr 从 C++14 开始可用,因此这里的 ctor 也可以是一个常量表达式。 两个重载operator()
分别是来自A
和B
的查找函数。 这些也可以是常量表达式,此类使我们能够在编译时和运行时执行双向查找:
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);
- 如何在没有映射的情况下在枚举和字符串之间进行转换?
- std::映射键作为模板化结构与枚举成员
- 如何使用枚举将字符值映射到 int
- 编译结构的时枚举映射
- 有效的双向作用域枚举映射
- 映射枚举值为C 中的模板参数
- 使用 C++11 可变参数模板初始化枚举到字符串映射
- 将枚举值映射到 C++ 中的类型
- 将C++枚举映射为常量字符*
- 在类中映射命名空间枚举
- 将一个枚举映射到另一个枚举
- 定义基于模板的映射指针,以便在验证范围时将 int 解析为枚举
- 在C++中如何将运行时类型鉴别器映射到模板实例(无需手动枚举所有实例)
- 枚举映射对重构具有鲁棒性
- 在模板专门化中映射枚举
- 使用C++中的函数指针映射枚举键和值
- 映射/联接两个自动生成的枚举的最佳方式
- 将存储在多映射中的私有枚举作为值读取
- 将字符串和枚举映射到模板化类型
- c++中的枚举映射