Andrew Tomazos Andrew Tomazos - 9 months ago 39
C++ Question

C++11 type to enum mapping?

I have an enum like:

enum E

And I want to create a compile-time mapping to get the appropriate E for a type like:

GetE<float> // returns TYPE_FLOAT
GetE<char> // returns TYPE_CHAR
GetE<int> // returns TYPE_INT

I thought of:

template<class T> struct GetE;

template<> struct GetE<float> { static constexpr E type = TYPE_FLOAT; };
template<> struct GetE<char> { static constexpr E type = TYPE_CHAR; };
template<> struct GetE<int> { static constexpr E type = TYPE_INT; };

But I'm getting errors like:

undefined reference to `GetE<int>::type'

Whats the best way to do this? And why the error?

Answer Source

It depends on how you use these constant expressions.

The ODR (one-definition rule) states that

(ยง3.2/2) [...] A variable whose name appears as a potentially-evaluated expression is odr-used unless it is an object that satisfies the requirements for appearing in a constant expression (5.19) and the lvalue-to-rvalue conversion (4.1) is immediately applied. [...]

(And then, lots of special rules, exceptions and exceptions of the exceptions follow.)

Any variable that is odr-used, must have exactly one definition. Your constant expressions have a declaration, but not a definition, so this goes well unless you odr-use one of them.

For example, the following goes well:

int main() {
  E e = GetE<float>::type;
  return 0;

But this does not:

void f(const E &)
{ }

int main() {
  return 0;

because f requires a (const) reference, so the lvalue-to-rvalue conversion cannot be applied immediately, hence this constitutes an odr-use. The compiler will complain that it misses a definition.

(Remark. As ShafikYaghmour found (see the comments), you may not get a complaint if the compiler uses optimization, as the references may be optimized away. To reproduce the compiler complaint, use the -O0 flag (or similar, depending on the compiler).)

To solve the problem, the required definition can be provided in the usual way, i.e. outside the struct-definition:

constexpr E GetE<float>::type;
constexpr E GetE<char>::type;
constexpr E GetE<int>::type;

But since this would have to happen in the .cpp (not the header file), you'll end up having to maintain the declarations and definitions in two different places, which is cumbersome.

The solution you've just suggested in your comment, i.e. define a constexpr (and inline) function, sounds right:

template <class T> constexpr E GetE();

template <> constexpr E GetE<float>()
{ return TYPE_FLOAT; }

template <> constexpr E GetE<char>()
{ return TYPE_CHAR; }

template <> constexpr E GetE<int>()
{ return TYPE_INT; }

void f(const E &)
{ }

int main() {
  E e = GetE<float>();


  return 0;