6
\$\begingroup\$

In the project I work on there are several places where a switch statement is used on a type enum. (I know, better to use virtual functions or a visitor pattern or something, but sometimes switching on type codes is unavoidable - e.g., deserializing XML.) All of our type enums are of the simple form enum { TypeA, TypeB, TypeC, NumTypes } so it is known at compile time that all type codes for a given type are in the half-open range [0, NumTypes).

I decided to see if I could make the compiler verify that all type codes were being checked, and this is what I came up with:

// Switch.h

template <int iIndex, int iCount, class Dispatcher>
struct Switcher
{
    typename Dispatcher::ResultType
    operator()( const Dispatcher& dispatcher, int iCase ) const
    {
        if ( iCase == iIndex )
        {
            return dispatcher.template Case<iIndex>();
        }
        else
        {
            return Switcher<iIndex + 1, iCount, Dispatcher>()( dispatcher, iCase );
        }
    }
};

template <int iCount, class Dispatcher>
struct Switcher<iCount, iCount, Dispatcher>
{
    typename Dispatcher::ResultType
    operator()( const Dispatcher& dispatcher, int iCase ) const
    {
        return dispatcher.Default( iCase );
    }
};

template <int iCount, class Dispatcher>
typename Dispatcher::ResultType
Switch( const Dispatcher& dispatcher, int iCase )
{
    return Switcher<0, iCount, Dispatcher>()( dispatcher, iCase );
}

Example use:

#include "Switch.h"

#include <iostream>
#include <cassert>
#include <string>

enum MyType { TypeA, TypeB, TypeC, NumTypes };

class MyDispatcher
{
public:
    typedef std::string ResultType;

    template <int iType>
    std::string Case() const;

    std::string Default( int iType ) const;
};

template <>
std::string
MyDispatcher::Case<TypeA>() const
{
    return "TypeA";
};

template <>
std::string
MyDispatcher::Case<TypeB>() const
{
    return "TypeB";
};

template <>
std::string
MyDispatcher::Case<TypeC>() const
{
    return "TypeC";
};

std::string
MyDispatcher::Default( int iType ) const 
{
    std::cout<< "ERROR: Unexpected type " << iType << std::endl;
    assert( false );
    return std::string();
}

int main() {
    MyType iType = TypeB;

    std::string zTypeName = Switch<NumTypes>( MyDispatcher(), iType );

    std::cout << "Type name: " << zTypeName << std::endl;
}

The Switcher template class will recursively instantiate versions of itself with increasing type codes, starting with zero and terminating at the iCount template parameter to the Switch function. Each instantiation will check its iIndex template parameter against the runtime iCase variable and call the dispatcher's Case<iIndex> function if it matches. The Default function will only be called if the runtime iCase variable is outside the expected range.

The key is that if any specialization of Case is missing in the dispatcher class, a linker error will be given complaining about a missing function. I know that many (most?) compilers can be set to produce warnings if not all members of an enum are handled in a switch statement, but there are too many places in the codebase (and likely in external headers) where not all cases are handled by design.

Thoughts? Any potential pitfalls? The system could be pretty easily extended to a range of enum values not starting at zero, but non-contiguous enum values would be a no-go.

\$\endgroup\$
2
  • \$\begingroup\$ You might want to edit your code so that it compiles in something other than MSVC. MSVC in non-compliant in this situation, specifically, explicit specializations must be at namespace scope. \$\endgroup\$
    – Yuushi
    Mar 19 '13 at 1:53
  • \$\begingroup\$ Thanks - should be OK now. I'm at work so I don't have direct access to GCC/Clang, but it compiles/runs OK at ideone.com (GCC 4.7.2). \$\endgroup\$ Mar 19 '13 at 16:53
5
\$\begingroup\$

That seems like an awful lot of boilerplate code to replace a single diagnostic that already exists in a lot of compilers. You're replacing

switch (fruit) {
  case APPLE: return "apple"; break;
  case PEAR: return "pear"; break;
  case PLUM: return "plum"; break;
}

with

<at file scope>
NEWSWITCH(uniqueID, std::string);
DEFCASE(uniqueID, APPLE) { return "apple"; }
DEFCASE(uniqueID, PEAR) { return "pear"; }
DEFCASE(uniqueID, PLUM) { return "plum"; }
DEFDEFAULT(uniqueID) { assert(false); }

...
Switch<NumTypes>(uniqueID(), fruit);

The most onerous requirement there is putting all the switch logic at file scope; you can't localize it to the actual codepath where it's going to be used. The second most onerous requirement is coming up with uniqueID, a global identifier which must be different for each switch that you convert using this pattern.

A much better solution to your original problem would be to use an array:

int main() {
    MyType iType = TypeB;

    std::string typenames[NumTypes] = { "TypeA", "TypeB", "TypeC" };

    assert(0 <= (int)iType && (int)iType < NumTypes);
    std::string zTypeName = typenames[iType];

    std::cout << "Type name: " << zTypeName << std::endl;
}

This code statically enforces that each index in 0..NumTypes-1 is associated with a std::string in the array; and (just like your original code) it dynamically asserts that iType is in the proper range.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.