Which Typesafe Enum in C++ Are You Using?
Asked Answered
E

11

45

It is common knowledge that built-in enums in C++ are not typesafe. I was wondering which classes implementing typesafe enums are used out there... I myself use the following "bicycle", but it is somewhat verbose and limited:

typesafeenum.h:

struct TypesafeEnum
{
// Construction:
public:
    TypesafeEnum(): id (next_id++), name("") {}
    TypesafeEnum(const std::string& n): id(next_id++), name(n) {}

// Operations:
public:
    bool operator == (const TypesafeEnum& right) const;
    bool operator != (const TypesafeEnum& right) const;
    bool operator < (const TypesafeEnum& right) const;

    std::string to_string() const { return name; }

// Implementation:
private:
    static int next_id;
    int id;
    std::string name;
};

typesafeenum.cpp:

int TypesafeEnum::next_id = 1;

bool TypesafeEnum::operator== (const TypesafeEnum& right) const 
{ return id == right.id; }

bool TypesafeEnum::operator!= (const TypesafeEnum& right) const 
{ return !operator== (right); }

bool TypesafeEnum::operator< (const TypesafeEnum& right) const  
{ return id < right.id; }

Usage:

class Dialog 
{
 ...
    struct Result: public TypesafeEnum
    {
        static const Result CANCEL("Cancel");
        static const Result OK("Ok");
    };


    Result doModal();
 ...
};

const Dialog::Result Dialog::Result::OK;
const Dialog::Result Dialog::Result::CANCEL;

Addition: I think I should have been more specific about the requirements. I'll try to summarize them:

Priority 1: Setting an enum variable to an invalid value should be impossible (a compile-time error) with no exceptions.

Priority 2: Converting an enum value to/from an int should be possible with a single explicit function/method call.

Priority 3: As compact, elegant and convenient declaration and usage as possible

Priority 4: Converting enum values to and from strings.

Priority 5: (Nice to have) Possibility to iterate over enum values.

Enscroll answered 20/10, 2008 at 4:58 Comment(4)
Sorry, what compiler do you use? MSVC 2008 cannot compile this example. This - static const Result CANCEL("Cancel"); - doesn't look like a valid C++ code...Chamfer
@Stiver: Sorry for long delay, probably not relevant any more, but I'll still answer: it was my mistake, the original version that I had didn't have strings, I bolted them on before posting without checking, sorry. The correct version would pass strings to the enum values' constructors in the cpp file.Enscroll
this is currently the best question on enums and strings in SO sofar.Kariekaril
I've changed the tags to c++03, as explicitly typed enums are now part of C++. Note that enums where already "typesafe" in C++2003.Server
H
44

I'm currently playing around with the Boost.Enum proposal from the Boost Vault (filename enum_rev4.6.zip). Although it was never officially submitted for inclusion into Boost, it's useable as-is. (Documentation is lacking but is made up for by clear source code and good tests.)

Boost.Enum lets you declare an enum like this:

BOOST_ENUM_VALUES(Level, const char*,
    (Abort)("unrecoverable problem")
    (Error)("recoverable problem")
    (Alert)("unexpected behavior")
    (Info) ("expected behavior")
    (Trace)("normal flow of execution")
    (Debug)("detailed object state listings")
)

And have it automatically expand to this:

class Level : public boost::detail::enum_base<Level, string>
{
public:
    enum domain
    {
        Abort,
        Error,
        Alert,
        Info,
        Trace,
        Debug,
    };

    BOOST_STATIC_CONSTANT(index_type, size = 6);

    Level() {}
    Level(domain index) : boost::detail::enum_base<Level, string>(index) {}

    typedef boost::optional<Level> optional;
    static optional get_by_name(const char* str)
    {
        if(strcmp(str, "Abort") == 0) return optional(Abort);
        if(strcmp(str, "Error") == 0) return optional(Error);
        if(strcmp(str, "Alert") == 0) return optional(Alert);
        if(strcmp(str, "Info") == 0) return optional(Info);
        if(strcmp(str, "Trace") == 0) return optional(Trace);
        if(strcmp(str, "Debug") == 0) return optional(Debug);
        return optional();
    }

private:
    friend class boost::detail::enum_base<Level, string>;
    static const char* names(domain index)
    {
        switch(index)
        {
        case Abort: return "Abort";
        case Error: return "Error";
        case Alert: return "Alert";
        case Info: return "Info";
        case Trace: return "Trace";
        case Debug: return "Debug";
        default: return NULL;
        }
    }

    typedef boost::optional<value_type> optional_value;
    static optional_value values(domain index)
    {
        switch(index)
        {
        case Abort: return optional_value("unrecoverable problem");
        case Error: return optional_value("recoverable problem");
        case Alert: return optional_value("unexpected behavior");
        case Info: return optional_value("expected behavior");
        case Trace: return optional_value("normal flow of execution");
        case Debug: return optional_value("detailed object state listings");
        default: return optional_value();
        }
    }
};

It satisfies all five of the priorities which you list.

Heaviness answered 13/1, 2009 at 13:51 Comment(6)
why is this needed to solve a problem that dosn't exist??!! head explodesManhour
No need for exploding heads; the complexity is nicely hidden behind macros and a clean interface. And standard C enums have several real problems: lack of type safety, no ability to query an enum type for its max size, lack of automatic conversion to/from strings.Heaviness
I originally designed boost::enum to be sort of a string table that was built at compile-time and could be used at run-time. Eventually it morphed into a more generic type-safe enum. It never got included with boost because of my lack of time to dedicate to documentation. Nice to see it out in the wild!Lamarre
This looks seriously useful, you should still try to get it in. Maybe a version that uses the new C++0x strongly typed enums (still nice to get the boost::optional interface and string conversion).Rumney
Yeh I second that, I just downloaded and started using it successfully in under an hour, very useful :)Columniation
BOOST_ENUM's in version 4.6 are not entirely type-safe, namely when it comes to operator ==(). E.g. BOOST_ENUM(Apple,(a)); BOOST_ENUM(Orange,(o)); bool x = Apple(Apple::a) == Orange::o; compiles just fine. The reason is that the ctor enum_base(index_type index) is not explicit, and index_type equals int. Not sure about other operators.Advowson
V
18

A nice compromise method is this:

struct Flintstones {
   enum E {
      Fred,
      Barney,
      Wilma
   };
};

Flintstones::E fred = Flintstones::Fred;
Flintstones::E barney = Flintstones::Barney;

It's not typesafe in the same sense that your version is, but the usage is nicer than standard enums, and you can still take advantage of integer conversion when you need it.

Volunteer answered 20/10, 2008 at 5:9 Comment(5)
Wouldn't this be cleaner with a namespace instead of a struct?Ambivert
Interesting thought. That would eliminate one occasional issue with this, which is having someone attempt to declare a Flintstones rather than a Flintstones::E.Volunteer
Follow-up on the namespace thing: for enums that live inside a class or struct, you can't use a namespace. If the enum doesn't live in a class or struct, I think you're right that using a namespace is cleaner.Volunteer
Another solution to that is to give the struct a private constructor, and it will work in the nested case.Rumney
c++11 enum classes make this entirely redundantVertebra
E
14

I use C++0x typesafe enums. I use some helper template/macros that provide the to/from string functionality.

enum class Result { Ok, Cancel};
Entomophilous answered 13/1, 2009 at 14:9 Comment(11)
Which compilers support this?Turgeon
Why not ask stackoverflow? :-) #934683Entomophilous
@Roddy: so basically you’re using a feature that just one compiler supports, on up-to-date systems (as opposed to most systems, which do not have gcc 4.4 yet)?Owens
@Konrad: No, I'm using one that at least two compilers support, (and I'm not using GCC 4.4). Code portability to other compilers is not an issue for me in my current projects.Entomophilous
I'm pretty sure the latest MSVC will support it too. If it works on Linux (GCC), OS X (GCC), and Windows (MSVC), that's 'good enough' portability for a fair number of users. Obviously not for everyone though ;)Rumney
@Joseph: Erm, which version of Visual Studio supports this? 2010 certainly doesn't, at least not for unmanaged code. And it means something vaguely different in the managed C++/CLI world.Masthead
@Cody: Looks like you're right: #2603814Entomophilous
@Cody: You're right, it's just because MSVC 2010 includes support for a bunch of other C++0x constructs that I thought they'd already implemented all the low hanging fruit.Rumney
@Joseph: Fair enough. I'd have expected the same, unfortunately it doesn't appear to be the case. Thus why I was browsing the answers to this question. :-) Maybe in the next version...Masthead
@CodyGray It is supported since MSVC 2012 ;)Franzoni
@Étienne Yeah. Unfortunately, color and lowercase letters are no longer supported.Masthead
M
6

I don't. Way too much overhead for little benefit. Also, being able to caste enumerations to different data types for serialization is a very handy tool. I have never seen an instance where a "Type safe" enumeration would be worth the overhead and complexity where C++ offers a good enough implementation already.

Manhour answered 20/10, 2008 at 5:4 Comment(2)
Don't shoot the messenger... Up voting againAdvent
A type safe enum can be implemented using a template such that there is no runtime overhead whatsoever. And if you use say, BOOST_ENUM, all the complexity is in a library.Rumney
R
2

My take is that you're inventing a problem and then fitting a solution onto it. I see no need to do an elaborate framework for an enumeration of values. If you are dedicated to having your values only be members of a certain set, you could hack up a variant of a unique set datatype.

Reina answered 20/10, 2008 at 15:9 Comment(0)
D
2

I'm personally using an adapted version of the typesafe enum idiom. It doesn't provide all the five "requirements" that you've stated in your edit, but I strongly disagree with some of them anyway. For example, I don't see how Prio#4 (conversion of values to strings) has anything to do with type safety. Most of the time string representation of individual values should be separate from the definition of the type anyway (think i18n for a simple reason why). Prio#5 (iteratio, which is optional) is one of the nicest things I'd like to see naturally happening in enums, so I felt sad that it appears as "optional" in your request, but it seems it is better addressed via a separate iteration system such as begin/end functions or an enum_iterator, which makes them work seamlessly with STL and C++11 foreach.

OTOH this simple idiom nicely provides Prio#3 Prio#1 thanks to the fact that it mostly only wraps enums with more type information. Not to mention it is a very simple solution that for the most part doesn't require any external dependency headers, so it's pretty easy to carry around. It also has the advantage of making enumerations scoped a-la-C++11:

// This doesn't compile, and if it did it wouldn't work anyway
enum colors { salmon, .... };
enum fishes { salmon, .... };

// This, however, works seamlessly.
struct colors_def { enum type { salmon, .... }; };
struct fishes_def { enum type { salmon, .... }; };

typedef typesafe_enum<colors_def> colors;
typedef typesafe_enum<fishes_def> fishes;

The only "hole" that solution provides is that it doesn't address the fact that it doesn't prevent enums of different types (or an enum and an int) from being directly compared, because when you use values directly you force the implicit conversion to int:

if (colors::salmon == fishes::salmon) { .../* Ooops! */... }

But so far I've found such problems can be solved by simply offering a better comparison to the compiler - for example, explicitly providing an operator that compares any two different enum types, then forcing it to fail:

// I'm using backports of C++11 utilities like static_assert and enable_if
template <typename Enum1, typename Enum2>
typename enable_if< (is_enum<Enum1>::value && is_enum<Enum2>::value) && (false == is_same<Enum1,Enum2>::value) , bool >
::type operator== (Enum1, Enum2) {
    static_assert (false, "Comparing enumerations of different types!");
}

Though it doesn't seem to break code so far, and it does to explicitly deal with the specific problem without doing something else, I'm not sure it such thing is a thing one "should" do (I suspect it will interfere with enums already taking part in conversion operators declared elsewhere; I'd gladly receive commentary about this).

Combining this with the above typesafe idiom gives something that is relatively close to C++11 enum class in humanibility (readability and maintainability) without having to do anything too obscure. And I have to admit it was fun to do, I had never thought to actually ask the compiler if I was dealing with enums or not...

Dael answered 8/8, 2012 at 2:8 Comment(2)
In your first code sample, you seem to be using a template class typesafe_enum - any chance of showing the code for that? It's hard to understand the answer otherwise (unless I'm missing something obvious).Manteau
Perhaps I wasn't obvious enough. It is basically the same code as the typesafe_enum in Wikibooks (which I've also linked to above). I added to that a couple of macros to both make declarations simpler and make it compatible with C++11. Perhaps I should post it somewhere.Dael
L
1

I think the Java enum would be a good model to follow. Essentially, the Java form would look like this:

public enum Result {
    OK("OK"), CANCEL("Cancel");

    private final String name;

    Result(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

What's interesting about the Java approach is that OK and CANCEL are immutable, singleton instances of Result (with the methods that you see). You cannot create any further instances of Result. Since they're singletons, you can compare by pointer/reference---very handy. :-)

ETA: In Java, instead of doing bitmasks by hand, instead you use an EnumSet to specify a bit set (it implements the Set interface, and works like sets---but implemented using bitmasks). Much more readable than hand-written bitmask manipulation!

Lathan answered 20/10, 2008 at 5:8 Comment(3)
That's very nice, but how can we do this in c++? And I think using addresses for comparison is not very harmless, consider if enum values get serialized on disk? Each time we might get different ordering.Enscroll
Remember, in Java, you can't do < comparisons on pointers, only == or !=. So as long as they're distinct, that's good enough to compare by pointer. :-) I'll see if I can write a C++ implementation sometime.Lathan
the java enum seems to be an instance of cope's curiously recurring pattern (en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern). naict by googleing, no one has tried this approach. i wonder if it is due to the difference between java generics and the stl semantics?Armindaarming
H
1

I gave an answer to this here, on a different topic. It's a different style of approach which allows most of the same functionality without requiring modification to the original enum definition (and consequently allowing usage in cases where you don't define the enum). It also allows runtime range checking.

The downside of my approach is that it doesn't programmatically enforce the coupling between the enum and the helper class, so they have to be updated in parallel. It works for me, but YMMV.

Hurtful answered 20/10, 2008 at 7:31 Comment(0)
Q
0

I am currently writing my own typesafe enum library at https://bitbucket.org/chopsii/typesafe-enums

I am not the most experienced C++ developer ever, but I am writing this due to the shortcomings of the BOOST vault enums.

Feel free to check it out and use them yourself, but they have some (hopefully minor) usability issues, and are probably not at all cross-platform.

Please contribute if you want to. This is my first open source undertaking.

Quadruplex answered 16/1, 2015 at 8:51 Comment(0)
F
0

Use boost::variant!

After trying a lot of the above ideas and finding them lacking I hit upon this simple approach:

#include <iostream>
#include <boost/variant.hpp>

struct A_t {};
static const A_t A = A_t();
template <typename T>
bool isA(const T & x) { if(boost::get<A_t>(&x)) return true; return false; }

struct B_t {};
static const B_t B = B_t();
template <typename T>
bool isB(const T & x) { if(boost::get<B_t>(&x)) return true; return false; }

struct C_t {};
static const C_t C = C_t();
template <typename T>
bool isC(const T & x) { if(boost::get<C_t>(&x)) return true; return false; }

typedef boost::variant<A_t, B_t> AB;
typedef boost::variant<B_t, C_t> BC;

void ab(const AB & e)
{
  if(isA(e))
    std::cerr << "A!" << std::endl;
  if(isB(e))
    std::cerr << "B!" << std::endl;
  // ERROR:
  // if(isC(e))
  //   std::cerr << "C!" << std::endl;

  // ERROR:
  // if(e == 0)
  //   std::cerr << "B!" << std::endl;
}

void bc(const BC & e)
{
  // ERROR:
  // if(isA(e))
  //   std::cerr << "A!" << std::endl;

  if(isB(e))
    std::cerr << "B!" << std::endl;
  if(isC(e))
    std::cerr << "C!" << std::endl;
}

int main() {
  AB a;
  a = A;
  AB b;
  b = B;
  ab(a);
  ab(b);
  ab(A);
  ab(B);
  // ab(C); // ERROR
  // bc(A); // ERROR
  bc(B);
  bc(C);
}

You can probably come up with a macro to generate the boilerplate. (Let me know if you do.)

Unlike other approaches this one is actually type-safe and works with old C++. You can even make cool types like boost::variant<int, A_t, B_t, boost::none>, for example, to represent a value that could be A, B, an integer or nothing which is almost Haskell98 levels of type safety.

Downsides to be aware of:

  • at-least with old boost -- I'm on a system with boost 1.33 -- you are limited to 20 items in your variant; there is a work-around however
  • affects compile time
  • insane error messages -- but that's C++ for you

Update

Here, for your convenience is your typesafe-enum "library". Paste this header:

#ifndef _TYPESAFE_ENUMS_H
#define _TYPESAFE_ENUMS_H
#include <string>
#include <boost/variant.hpp>

#define ITEM(NAME, VAL) \
struct NAME##_t { \
  std::string toStr() const { return std::string( #NAME ); } \
  int toInt() const { return VAL; } \
}; \
static const NAME##_t NAME = NAME##_t(); \
template <typename T> \
bool is##NAME(const T & x) { if(boost::get<NAME##_t>(&x)) return true; return false; } \


class toStr_visitor: public boost::static_visitor<std::string> {
public:
  template<typename T>
  std::string operator()(const T & a) const {
    return a.toStr();
  }
};

template<BOOST_VARIANT_ENUM_PARAMS(typename T)>
inline static
std::string toStr(const boost::variant<BOOST_VARIANT_ENUM_PARAMS(T)> & a) {
  return boost::apply_visitor(toStr_visitor(), a);
}

class toInt_visitor: public boost::static_visitor<int> {
public:
  template<typename T>
  int operator()(const T & a) const {
    return a.toInt();
  }
};

template<BOOST_VARIANT_ENUM_PARAMS(typename T)>
inline static
int toInt(const boost::variant<BOOST_VARIANT_ENUM_PARAMS(T)> & a) {
  return boost::apply_visitor(toInt_visitor(), a);
}

#define ENUM(...) \
typedef boost::variant<__VA_ARGS__>
#endif

And use it like:

ITEM(A, 0);
ITEM(B, 1);
ITEM(C, 2);

ENUM(A_t, B_t) AB;
ENUM(B_t, C_t) BC;

Notice you have to say A_t instead of A in the ENUM macro which destroys some of the magic. Oh well. Also, notice there's now a toStr function and a toInt function to meet OPs requirement of simple conversion to strings and ints. The requirement I can't figure out is a way to iterate over the items. Let me know if you know how to write such a thing.

Feaster answered 6/10, 2016 at 19:49 Comment(0)
O
-2

Not sure if this post is too late, but there's an article on GameDev.net which satisfies all but the 5th point (ability to iterate over enumerators): http://www.gamedev.net/reference/snippets/features/cppstringizing/

The method described by the article allows string conversion support for existing enumerations without changing their code. If you only want support for new enumerations though, I'd go with Boost.Enum (mentioned above).

Outreach answered 7/6, 2009 at 23:52 Comment(1)
I think this is the new link: gamedev.net/page/resources/_/technical/general-programming/…Isotron

© 2022 - 2024 — McMap. All rights reserved.