views:

158

answers:

8

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 the 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?

A: 

If all of the elements are in the same order, and specific ordinals are not given (eg FOO = 42) you can just use the integer value casted to the new Enum type.

Eric
The OP specifically said "...equivalent but numbered differently" so you can't just recast the value.
Mark B
A: 

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.

cake
+2  A: 

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);
}
Josh Kelley
That's basically boost preprocessor magic except reinventing boost preprocessor. Give it a look, it's a lot less ugly than doing X macros yourself: http://www.boost.org/doc/libs/1_43_0/libs/preprocessor/doc/index.html
Joseph Garvin
You wanted a solution that avoids the slow compile times and difficult-to-diagnose errors of Boost.Preprocessor. This solution does so. Boost.Preprocessor is a good tool, and I've used it before, I'm just not convinced that its complexity and overhead are justified in this case.
Josh Kelley
Err, AFAICT this is the same technique Boost.Preprocessor uses internally; I don't see how this would be any faster to compile.
Joseph Garvin
Boost.Preprocessor uses the same techniques internally, but to present its nice interface, it relies on large numbers of header files, hundreds of macro definitions, expansions of expansions of expansions, etc. I've seen Boost.Preprocessor macros that take tens of thousands of steps to fully expand. I doubt that what you're trying to do (as outlined in @Matthieu M.'s answer) would be quite so bad, but I doubt it would be as fast as simply including a file a few times, either.
Josh Kelley
A: 

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.

Dan Olson
Unfortunately it's out of my hands. Consider an API that works based on the user passing in values from an enum, EnumA. Originally, the API is implemented with a backend that uses EnumA. Later on, you decide to use a different implementation that uses an entirely different enum internally, EnumB. To maintain backwards compatibility, you need to be able to translate EnumA<->EnumB.
Joseph Garvin
+2  A: 

Why won't a lookup table work? Why are you forced to use this gigantic switch statement??

Noah Roberts
A lookup table would work fine if you could generate a bidirectional one that was const. Otherwise you're paying a runtime cost for information that is known at compile time.
Joseph Garvin
So initialize the lookup table at program startup; a one-time startup-only runtime cost should be negligible. Or generate a const lookup table plus the enum definitions from a script if you want to avoid all runtime costs.
Josh Kelley
You mean "bidirectional" as in there's two of them?
Noah Roberts
A: 

When does this ever occur?

John
Your remark should be a comment, not an answer. It doesn't address the question at all. Also, see my comment on Dan Olson's answer for an example.
Joseph Garvin
Who cares whether it's a "post" or a "comment"? Besides, even if I bothered to guess where YOU want me to post/comment... it's a quirk of this site: unregistered users can't comment except on their own post.
John
+2  A: 

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
Matthieu M.
Unrelated: names ending in `_t` are reserved.
Jon Purdy
@Jon Purdy: ah crap! I only knew of `*__*` and `_[A-Z]*` :/ I'll just remove the typedefs then.
Matthieu M.
+1  A: 

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;
}
Mark B