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.
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 class
es, 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 enum
s 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With