Udostępnij za pośrednictwem


Discriminated Unions in C++

Earlier this week I started writing a function which needed to represent three states in the return value, two of which had an associated value.  In my mind I immediately came to the following solution

 type BuildAction =
    | Reset
    | LinkOneWithNext of Statement
    | LinkManyWithNext of Statement seq

A discriminated union is perfectly suited for representing this type of scenario.  Unfortunately for me I was coding in C++ and not F# so I shifted gears and started on a different solution.  But I quickly grew frustrated with new solution and decided to backtrack.  A discriminated union is just too ideal for this scenario so why not spend fifteen minutes and see how far I could get in defining a discriminated union structure in C++.

Of course a full discriminated union in the style of F# is not possible because C++ lacks language support for pattern matching.  But I was willing to live with that and other limitations as long as I could get many of the other benefits I find with discriminated unions.  In particular

  • Declarative Syntax
  • Typed value access without the need to cast
  • Allow mixing of flag only and flags with values
  • Customized naming of entries (no tuple style First, Second, etc …)
  • Instances are read only

After a bit of tinkering I came up with a solution (full source at end of post) which allows for the following declaration syntax

 DECLARE_DISCRIMINATED_UNION(BuildAction)
    DISCRIMINATED_UNION_FLAG(BuildAction, Reset)
    DISCRIMINATED_UNION_VALUE(BuildAction, LinkOneWithNext, Statement)
    DISCRIMINATED_UNION_VALUE(BuildAction, LinkManyWithNext, vector<Statement>)
END_DISCRIMINATED_UNION()

Each declared entry in the union is provided the following methods. 

  • A static factory method for creating values: CreateLinkOneWithNext
  • A method to test to see if an instance is the value type: IsLinkOneWithNext
  • A method to get the value associated with the associated value: GetLinkOneWithNext

 

 auto action = BuildAction::CreateLinkOneWithNext(statement);

if (action.IsLinkOneWithNext()) {
    Method(action.GetLinkOneWithNext());
}

It’s certainly far from perfect.  But it did give me the tools to implement the solution in the way I inherently thought about it and freed me to spend my thinking time on other problems.

EDIT

A couple of people asked why I didn't use boost::variant for this solution?  Ideally this is the approach I would've taken.  But for this particular scenario boost was unfortunately not an option (no weird management issues, just a boring production environment one).  

Additionally the spirit of this post and experiment was just having a bit of fun and sharing the results (even though it does contain a few pieces of evil code). 

DiscriminatedUnion.h

 //----------------------------------------------------------------------------
//
// Discriminated Union in C++.
//
//----------------------------------------------------------------------------
#ifdef DECLARE_DISCRIMINATED_UNION
#undef DECLARE_DISCRIMINATED_UNION
#endif

#ifdef END_DISCRIMINATED_UNION
#undef END_DISCRIMINATED_UNION
#endif

#ifdef DISCRIMINATED_UNION_VALUE
#undef DISCRIMINATED_UNION_VALUE
#endif

#ifdef DISCRIMINATED_UNION_POINTER
#undef DISCRIMINATED_UNION_POINTER
#endif

#ifdef DISCRIMINATED_UNION_ALLOW_NONE
#undef DISCRIMINATED_UNION_ALLOW_NONE
#endif

#ifdef DISCRIMINATED_UNION_GET_KIND
#undef DISCRIMINATED_UNION_GET_KIND
#endif

#define DECLARE_DISCRIMINATED_UNION(name)       \
    struct name {                               \
    private:                                    \
        name() {}                               \
        unsigned __int32 m_kind;                \
    public:

#define DECLARE_DISCRIMINATED_UNION_WITH_NONE(name)         \
    struct name {                                           \
    private:                                                \
        unsigned __int32 m_kind;                            \
    public:                                                 \
        name() : m_kind(__LINE__) {}                        \
        bool IsNone() const {return m_kind == __LINE__;}

#define DISCRIMINATED_UNION_VALUE(unionName, entryName, entryType)                                          \
        static unionName Create##entryName(const entryType& value) {                                        \
            unionName unionValue;                                                                           \
            unionValue.m_kind = __LINE__;                                                                   \
            unionValue.m_##entryName = value;                                                               \
            return unionValue;  }                                                                           \
        bool Is##entryName() const { return m_kind == __LINE__;}                                            \
        const entryType& Get##entryName() const { ASSERT(m_kind == __LINE__); return m_##entryName; }       \
        entryType Get##entryName() { ASSERT(m_kind == __LINE__); return m_##entryName; }                    \
    private:                                                                                                \
        entryType m_##entryName;                                                                            \
    public:

#define DISCRIMINATED_UNION_POINTER(unionName, entryName, entryType)                                        \
        static unionName Create##entryName(entryType* value) {                                              \
            unionName unionValue;                                                                           \
            unionValue.m_kind = __LINE__;                                                                   \
            unionValue.m_##entryName = value;                                                               \
            return unionValue;  }                                                                           \
        bool Is##entryName() const { return m_kind == __LINE__;}                                            \
        entryType* Get##entryName() const { ASSERT(m_kind == __LINE__); return m_##entryName; }             \
        entryType* Get##entryName() { ASSERT(m_kind == __LINE__); return m_##entryName; }                   \
    private:                                                                                                \
        entryType* m_##entryName;                                                                           \
    public:

#define DISCRIMINATED_UNION_FLAG(unionName, entryName)                                                      \
        static unionName Create##entryName() {                                                              \
            unionName unionValue;                                                                           \
            unionValue.m_kind = __LINE__;                                                                   \
            return unionValue;  }                                                                           \
        bool Is##entryName() const { return m_kind == __LINE__;}                                            

#define DISCRIMINATED_UNION_GET_KIND() unsigned __int32 GetKind() const { return m_kind; }

#define END_DISCRIMINATED_UNION() };

Comments

  • Anonymous
    November 17, 2010
    Yes, in a world of separate compilation units, that use of LINE certainly merits the 'Evil' tag.   It is a tempting sort of evil though.

  • Anonymous
    November 18, 2010
    Are you allowed to use Boost in Microsoft?  Boost's Variant library provides what you need, in a clean C++ way. struct Reset{}; typedef Statement LinkOneWithNext; typedef std::vector<Statement> LinkManyWithNext; typedef boost::variant<Reset, LinkOneWithNext, LinkManyWithNext> BuildAction; You can then use visitors (a class with overloaded operator() for each type), get<LinkOneWithNext>(v), v.type(), etc. to access it.

  • Anonymous
    November 18, 2010
    @Cory, Boost is fine to use in general.  In this specific project though I cannot use boost (not for any kind of weird management reasons but for weirder infrastructure ones).   If I could though I would have gone with boost::variant over my ... interesting macro.  

  • Anonymous
    November 30, 2010
    Why are so many people using Boost? I've tried to used boost but as soon as I dug into it's source I was overwhelmed by many layers of macros, includes templates. If something is wrong I would rather be able to debug it in a resonable time =) So I would rather use either my small custom classes or other libs