Reputation: 691
Having:
enum class A : int {
FirstA,
SecondA,
InvalidB
};
enum class B : int {
FirstB,
SecondB,
InvalidB
};
How to enable something like this?
B b = mapper[A::FirstA];
A a = mapper[B::SecondB];
One possible solution is to create a Mapper
template class, which allows to specify the mapping via initializer list in the constructor, something like:
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
);
But internally this will require compromises - either two maps (from A
to B
and from B
to A
) or one map, e.g. from A
to B
and a linear search for B
to A
conversions).
Is it possible to implement this in standard C++14
so, that:
With the requirements, that:
A
are not mapped to the same underlying values of B
A
and B
may differ?
Upvotes: 2
Views: 648
Reputation: 2880
If you need to perform the run-time lookup, the following method would work with the complexity O(1) in both directions.
Since all your enumerators of A
and B
are not initialized, the first enumerator has the value of zero, the second one has the value of 1
, and so on.
Regarding these zero-starting integers as indices of arrays, we can construct a bidirectional map using two arrays.
For instance, assuming the current mapping as
A::FirstA (=0) <--> B::SecondB (=1),
A::SecondA (=1) <--> B::FirstB (=0),
, then let us define the following two arrays
A arrA[2] = {A::SecondA, A::FirstA},
B arrB[2] = {B::SecondB, B::FirstB},
where arrA[i]
is the enumerator of A
corresponding to i
-th enumerator of B
, and vice versa.
In this setup, we can perform a lookup from A a
to B
as arrB[std::size(a)]
, and vice versa, with the complexity O(1).
The following class biENumMap
is an implementation example of the above bidirectional method with C++14 and over.
Please note that since the extended constexpr is available from C++14, here the ctor also can be a constant expression.
Two overloads operator()
are lookup functions from A
and B
, respectively.
These can also be constant expressions and this class enables us to perform bidirectional lookup at both compile-time and run-time:
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)];
}
};
We can use this class as follows:
// 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);
Upvotes: 2
Reputation: 23681
Nathan's solution is hard to beat in terms of implementation elegance. But if you desperately need a solution that doesn't rely on macros or that can also be used at run-time, here is one where you specify the mapping in a simple pair list.
At the core of it, we are using the fact that both enums should have contiguous underlying integral values (starting at zero), which means we can represent the mappings in both directions as simple arrays. This is all constexpr
so zero overhead in the compile-time case. For usage at runtime this does store the info twice to allow instant lookup, but only takes N (sizeof(A) + sizeof(B))
storage. I don't know any data structure that does better (i.e. does not store any additional data beyond one of the two arrays and is better than linear search in both directions). Note that this is takes the same storage as storing the pairs themselves (but is not gaining anything from the bijectivity of the mapping).
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();
};
(This uses fold expressions + comma operator to assign values the array elements, see e.g. here).
User code provides a list of mapping pairs to use and is done:
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{});
Demo including full compile-time test cases and maximally efficient runtime code (literally just mov
).
Here is a previous version that works only at compile-time (see also revision history): https://godbolt.org/z/GCkAhn
Upvotes: 1
Reputation: 180500
You can do this pretty easily using function templates and full specialization. You make the primary template return the invalid case and then the specializations would return the mappings that you want.
If you have
template<A>
B mapper() { return B::InvalidB; }
template<B>
A mapper() { return A::InvalidA; }
then you can add all of the mapped values like
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; }
and then you would call it like
B b = mapper<A::FirstA>();
A a = mapper<B::SecondB>();
This leaves you with no containers at all. You can even make some macros to make this easier like
#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; }
and then you would use them like
MAKE_ENUM_MAP(A, B)
ADD_MAPPING(A::FirstA, B::SecondB)
ADD_MAPPING(A::SecondA, B::FirstB)
to generate all the code for you. The above version uses a single enum value of Invalid
for the invalid case of the mappings. If you don't want that you can add to the macro what from
and to
values to use for invalid mappings like
#define MAKE_ENUM_MAP(from, from_value, to, to_value) \
template<from> \
auto mapper() { return to_value; } \
template<to> \
auto mapper() { return from_value; }
and you would call it like
MAKE_ENUM_MAP(A, A::InvalidA, B, B::InvalidB)
Upvotes: 2