Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this pattern ok for a source backward-compatible migration from C++03 enum into C++11 enum class?

We are about to migrate (around the next two years) all our compilers to C++11-ready compilers.

Our clients will use our headers, and we are now on the position to write (more or less from scratch) headers for our new API.

So we must choose between keeping to C++03 enums (with all their warts), or use a wrapping class to simulate C++11 notation because we want, in the end, to move those enums to C++11.

Is the "LikeEnum" idiom proposed below a viable solution, or are there unexpected surprises hiding behind it?

template<typename def, typename inner = typename def::type>
class like_enum : public def
{
  typedef inner type;
  inner val;

public:

  like_enum() {}
  like_enum(type v) : val(v) {}
  operator type () const { return val; }

  friend bool operator == (const like_enum & lhs, const like_enum & rhs) { return lhs.val == rhs.val; }
  friend bool operator != (const like_enum & lhs, const like_enum & rhs) { return lhs.val != rhs.val; }
  friend bool operator <  (const like_enum & lhs, const like_enum & rhs) { return lhs.val <  rhs.val; }
  friend bool operator <= (const like_enum & lhs, const like_enum & rhs) { return lhs.val <= rhs.val; }
  friend bool operator >  (const like_enum & lhs, const like_enum & rhs) { return lhs.val >  rhs.val; }
  friend bool operator >= (const like_enum & lhs, const like_enum & rhs) { return lhs.val >= rhs.val; }
};

Which would enable us to upgrade our enums without needing undesirable changes in the user code:

//    our code (C++03)                   |     our code C++11
// --------------------------------------+---------------------------
                                         |
struct KlingonType                       | enum class Klingon
{                                        | {
   enum type                             |    Qapla,
   {                                     |    Ghobe,
      Qapla,                             |    Highos
      Ghobe,                             | } ;
      Highos                             |
   } ;                                   |
} ;                                      |
                                         |
typedef like_enum<KlingonType> Klingon ; |
                                         |
// --------------------------------------+---------------------------
//                client code (both C++03 and C++11)

void foo(Klingon e)
{
   switch(e)
   {
      case Klingon::Qapla :  /* etc. */ ; break ;
      default :              /* etc. */ ; break ;
   }
}

Note: The LikeEnum was inspired by the Type Safe Enum idiom

Note 2: Source compatibility doesn't cover compilation error because of implicit conversion to int: Those are deemed undesirable, and the client will be notified in advance to make to-integer conversion explicit.

like image 877
paercebal Avatar asked Apr 24 '14 13:04

paercebal


1 Answers

The short answer is yes, that is a viable solution (with one fix).

Here's the long answer. :)


You have a compile-time error with your comparison functions, strictly speaking. This will cause portability issues with standard-compliant compilers. In particular, consider the following:

bool foo(Klingon e) { return e == Klingon::Qapla }

The compiler shouldn't know which overload of operator== to use, as both converting e to KlingonType::type implicitly (via operator type() const) and converting Klingon::Qapla to Klingon implicitly (via Klingon(type)) need one conversion.

Requiring operator type() const to be explicit will fix this error. Of course, explicit doesn't exist in C++03. This means you'll have to do as @Yakk suggests in the comments and use something similar to the safe-bool idiom for inner's type. Removing operator type() const entirely is not an option because it would remove explicit conversions to integral types.

Since you say you're fine with implicit conversions still being possible, a simpler fix would be to also define comparison functions with the underlying enum type. So in addition to:

friend bool operator == (const like_enum & lhs, const like_enum & rhs) { return lhs.val == rhs.val; }
friend bool operator != (const like_enum & lhs, const like_enum & rhs) { return lhs.val != rhs.val; }
friend bool operator <  (const like_enum & lhs, const like_enum & rhs) { return lhs.val <  rhs.val; }
friend bool operator <= (const like_enum & lhs, const like_enum & rhs) { return lhs.val <= rhs.val; }
friend bool operator >  (const like_enum & lhs, const like_enum & rhs) { return lhs.val >  rhs.val; }
friend bool operator >= (const like_enum & lhs, const like_enum & rhs) { return lhs.val >= rhs.val; }

you'll also need:

friend bool operator ==(const like_enum& lhs, const type rhs) { return lhs.val == rhs; }
friend bool operator !=(const like_enum& lhs, const type rhs) { return lhs.val != rhs; }
friend bool operator < (const like_enum& lhs, const type rhs) { return lhs.val <  rhs; }
friend bool operator <=(const like_enum& lhs, const type rhs) { return lhs.val <= rhs; }
friend bool operator > (const like_enum& lhs, const type rhs) { return lhs.val >  rhs; }
friend bool operator >=(const like_enum& lhs, const type rhs) { return lhs.val >= rhs; }
friend bool operator ==(const type lhs, const like_enum& rhs) { return operator==(rhs, lhs); }
friend bool operator !=(const type lhs, const like_enum& rhs) { return operator!=(rhs, lhs); }
friend bool operator < (const type lhs, const like_enum& rhs) { return operator> (rhs, lhs); }
friend bool operator <=(const type lhs, const like_enum& rhs) { return operator>=(rhs, lhs); }
friend bool operator > (const type lhs, const like_enum& rhs) { return operator< (rhs, lhs); }
friend bool operator >=(const type lhs, const like_enum& rhs) { return operator<=(rhs, lhs); }

After fixing the above, there is little noticeable difference semantically (ignoring implicit conversions being possible). The only difference I found is the value of std::is_pod<Klingon>::value from <type_traits> in C++11. Using the C++03 version, this will be false, whereas using enum classes, this will be true. In practice, this means (without optimizations) that a Klingon using enum class can be carried around in a register whereas the like_enum version will need to be on the stack.

Because you don't specify the underlying representation of the enum class, sizeof(Klingon) will probably be the same for both, but I wouldn't rely on it. The unreliability of the underlying representation chosen by different implementations was part of the motivation behind strongly-typed enums after all.

Here's proof of the above two paragraphs for clang++ 3.0+, g++ 4.5+, and msvc 11+.


Now in terms of the compiled output, both obviously will have incompatible ABI. This means your entire codebase needs to use either one or the other. They will not mix. For my system (clang++-3.5 on OSX), the above function's symbol is __Z1f9like_enumI11KlingonTypeNS0_4typeEE for the C++03 version and __Z1f7Klingon for the C++11 version. This should only be an issue if these are exported library functions.

The outputted assembly is identical in my testing for clang++ and g++ after turning optimizations to -O2. Presumably other optimizing compilers will also be able to unwrap Klingon to KlingonType::type. Without optimizations, the enum class version will still of course avoid all the constructor and comparison operator function calls.

like image 198
tclamb Avatar answered Nov 15 '22 16:11

tclamb