How to optimally translate enums in the smallest amount of code?
It's often the case that you have 2 enums that are equivalent but numbered differently, and you need functions to translate elements from the first enum to elements of t开发者_如何学运维he second and the reverse. This is usually really tedious to code, and anytime a new element is added you have to make sure to add the pairing both to the converting and to the reverse-converting function (it violates the DRY principle).
What's the least error prone way to do this that still generates efficient code? I mention the efficient part because you could just make a bunch of pairs and put them in std::maps if runtime lookups weren't an issue. I'd prefer something that's as performant as manually writing big switch statements going from one enum value to the other.
With some boost preprocessor magic or some template hackery I'm sure you could come up with something where you write a list of pairs and the converting and reverse-converting functions are generated, but I'm not sure which approach to prefer or why. Both methods have a reputation for slow compile times and difficult to diagnose compile errors. Or maybe some other approach entirely?
As Neil said, I never faced the problem myself. I have faced the issue with codesets though (ie mapping from a string to an enum, and back).
My first reaction is epidermic: DRY.
As you noted, maintaining two enums and the translation takes time, so it's better to refactor and use only one.
My second reaction would be to try and remove the enum. I don't like enums. Perhaps I'll appreciate them with the upcoming C++0x, but as it stands they're more trouble than they are worth in my mind. I prefer smart objects, with categories, etc...
But then, the problem itself is amusing I guess. So if you like to deal with this messy situation, I might as well try to ease your burden.
Templates can't do much here. I've used them for range checking on enums and for string conversion (back and forth) and iterations, but that's all they can do. However as you suspected it's possible with some "subtle" application of Boost.Preprocessor.
What you'd like to write:
DEFINE_CORRESPONDING_ENUMS(Server, Client,
((Server1, 1, Client1, 6))
((Server2, 2, Client2, 3))
((Common1, 4, Common1, 4))
((Common2, 5, Common2, 5))
((Server3, 7, Client3, 1))
);
And we would like it to generate:
struct Server
{
enum type { Server1 = 1, Server2 = 2, Common1 = 4, Common2 = 5, Server3 = 7 };
};
struct Client
{
enum type { Client1 = 6, Client2 = 3, Common1 = 4, Common2 = 5, Client3 = 1 };
};
Server::type ServerFromClient(Client::type c)
{
switch(c)
{
case Client1: return Server1;
//...
default: abort();
}
}
Client::type ClientFromServer(Server::type s)
{
//...
}
The good news is, this is possible. I could even do it, though I'll probably let you work on it a bit ;)
Here are some explanations:
- The third element of the macro is a sequence. A sequence is of unbounded size.
- Each element of the sequence is a 4-tuple. You need to know its size in advance, thus the repetition for
Common
. If you used a sequence instead, you could deal with a variable number of elements (for example to avoid repeating the common...) but it would make things much more complicated - You'll need to look at
BOOST_PP_SEQ_FOREACH
, it'll be the basic operation here. - Don't forget
BOOST_PP_CAT
to handle concatenation of tokens. - on
gcc
the-E
option yields the preprocessor output, it may come handy... - don't forget comments and how to use in the file, your colleagues will hate you otherwise
Are you looking for something like this? Not tested, but it should work.
(Standard warnings about premature optimizations and the need for profiling apply; a std::map lookup may not be that bad, a giant switch table may not be that good.)
enums-impl.h:
// No include guard.
DEFINE_ENUM_PAIR(EGA_BRIGHT_RED, 12, HTML_RED, 0xff0000)
DEFINE_ENUM_PAIR(EGA_BRIGHT_BLUE, 9, HTML_BLUE, 0x0000ff)
DEFINE_ENUM_PAIR(EGA_BRIGHT_GREEN, 10, HTML_GREEN, 0x00ff00)
DEFINE_ENUM_PAIR(EGA_BLACK, 0, HTML_BLACK, 0x000000)
enums.cpp:
enum EgaColorType {
#define DEFINE_ENUM_PAIR(name1, value1, name2, value2) name1 = value1,
#include "enums-impl.h"
#undef DEFINE_ENUM_PAIR
};
enum HtmlColorType {
#define DEFINE_ENUM_PAIR(name1, value1, name2, value2) name2 = value2,
#include "enums-impl.h"
#undef DEFINE_ENUM_PAIR
};
HtmlColorType ConvertEgaToHtml(EgaColorType c) {
switch (c) {
#define DEFINE_ENUM_PAIR(name1, value1, name2, value2) case name1: return name2;
#include "enums-impl.h"
#undef DEFINE_ENUM_PAIR
default: assert(false);
}
EgaColorType ConvertHtmlToEga(HtmlColorType c) {
switch (c) {
#define DEFINE_ENUM_PAIR(name1, value1, name2, value2) case name2: return name1;
#include "enums-impl.h"
#undef DEFINE_ENUM_PAIR
default: assert(false);
}
Why won't a lookup table work? Why are you forced to use this gigantic switch statement??
If the enumeration ranges are relatively dense (rather than being used as bitmap indicators), you can just use an array to do the mapping. You let the compiler figure out the array length and then you can assert if the length isn't what you want. You might even be able to static_assert it, I'm not sure. Since you're using arrays, the conversion should be constant time, and possibly better than a switch if the compiler doesn't generate a jump table internally. Note that this code is totally untested.
enum A
{
MIN_A = 1,
A_ATT_1 = 1,
A_ATT_2 = 2,
A_ATT_3 = 3,
LAST_A
};
enum B
{
MIN_B = 2
B_ATT_2 = 2,
B_ATT_1 = 4,
B_ATT_3 = 5,
LAST_B
};
B A_to_B[] =
{
B_ATT_1,
B_ATT_2,
B_ATT_3
};
// Somewhere that will always run, as desired:
assert(LAST_A - MIN_A == sizeof(A_to_B) / sizeof(A_to_B[0]);
B from_A(A in)
{
B ret = A_to_B[in - MIN_A];
assert(ret != LAST_B);
return ret;
}
A B_to_A[] =
{
A_ATT_2,
LAST_A,
A_ATT_1,
A_ATT_3
};
// Somewhere that will always run, as desired:
assert(LAST_B - MIN_B == sizeof(B_to_A) / sizeof(B_to_A[0]);
A from_B(B in)
{
A ret = B_to_A[in - MIN_B];
assert(ret != LAST_A);
return ret;
}
Well, you can always try to make a function (in the sense of mathematical function, not programming function), that translates the number of one enum to the other. But this function would change every time you add elements, though.
Consider not using two enums.
There's not much difference between these:
enum FirstSet { A=4, B=6, C=8, D=5 };
enum SecondSet { E=2, F=5, G=5, H=1 };
and this:
enum OneEnum { A, B, C, D };
enum TwoEnum { E, F, G, H };
int FirstSet[] = { 4, 6, 8, 5 };
int SecondSet[] = { 2, 5, 5, 1 };
The number of accesses that need changing may be prohibitive, but this is a bit better than an O(n) lookup every time you want to convert.
精彩评论