Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Switching on custom objects

4.88/5 (16 votes)
14 Nov 2014Public Domain3 min read 14.7K  
A switch-like construct for custom objects to improve readability.

Introduction

Sometimes we have a series of if-then statements in our code where a switch statement would be clearer. C++ doesn’t support switching on non-integral values and so we end up with a series of if-then statements instead. We might also not bother to optimize those if-then statements (utilizing a map, hash-table or other quick lookup data structure) because we don’t deem those parts of the code performance critical. In such places, it might be clearer to the maintainer of the code to have something akin to a switch statement instead.

In order to improve readability, we can use a fluent interface via method chaining. A macro based solution would be another approach, but it would be harder to debug and we wouldn’t get code completion.

Design

Let’s think of what syntax we would like to have and work towards it.

  • We want to be able to switch on any object which has operator equal to, defined.
  • We don't want case expressions to be restricted to compile time constants.
  • We want to be able to fall through implicitly and break explicitly.
  • Finally, we want to have a default.

The following code illustrates the features we want:

C++
int main()
{
    cout << "day?" << endl;

    string day;
    cin >> day;

    Switch(day)
        .Case("monday")
        .Case("tuesday")
        .Case("wednesday")
        .Case("thursday")
        .Case("friday", [] { cout << "weekday" << endl; }).Break()
        .Case("sunday")
        .Case("saturday", [] { cout << "weekend" << endl; }).Break()
        .Default([] { cout << "unknown day" << endl; });

    return 0;
}

Implementation

Now that we’ve decided how it should look in action, we can think about the implementation itself. We would need to have a templated function, Switch. Further, it would return an object which has the functions Case, Break and Default. These functions would mutate the object and then return a reference to it, so that the functions can be chained.

Turns out, it’s quite easy to implement:

C++
#pragma once

#include <functional>

template <class T>
class CaseBreakDefault
{
public:
    typedef std::function<void ()> Block;

private:
    const T &m_value;
    bool m_match;
    bool m_break;

public:
    explicit CaseBreakDefault(const T &value) :
        m_value(value),
        m_match(false),
        m_break(false)
    {
    }

    CaseBreakDefault<T> &Case(const T &value, const Block &block)
    {
        if( m_break )
            return *this;

        if( m_value == value )
            m_match = true;

        if( m_match && block)
            block();

        return *this;
    }

    CaseBreakDefault<T> &Case(const T &value)
    {
        return Case(value, nullptr);
    }

    CaseBreakDefault<T> &Break()
    {
        if( m_match )
            m_break = true;

        return *this;
    }

    void Default(const Block &block)
    {
        if( m_break )
            return;

        if( block )
            block();
    }

private:
    void operator =(const CaseBreakDefault &);
};

template <class T>
CaseBreakDefault<T> Switch(const T &value)
{
    return CaseBreakDefault<T>(value);
}

A no-nonsense interface

We can improve the design of the interface however, to restrict it from inadvertently being used illogically. We don’t want to allow a break statement to immediately follow the switch statement or another break statement. To this end, we will derive another class from CaseBreakDefault which hides the break statement. This derived class called CaseDefault (for all the right reasons) would be the type of object returned from the Switch and Break functions.

The final code (with the changes in bold) is shown below for completeness.

C++
#pragma once

#include <functional>

template <class T>
class CaseDefault;

template <class T>
class CaseBreakDefault
{
public:
    typedef std::function<void ()> Block;

private:
    const T &m_value;
    bool m_match;
    bool m_break;

public:
    explicit CaseBreakDefault(const T &value) :
        m_value(value),
        m_match(false),
        m_break(false)
    {
    }

    CaseBreakDefault<T> &Case(const T &value, const Block &block)
    {
        if( m_break )
            return *this;

        if( m_value == value )
            m_match = true;

        if( m_match && block)
            block();

        return *this;
    }

    CaseBreakDefault<T> &Case(const T &value)
    {
        return Case(value, nullptr);
    }

    CaseDefault<T> &Break()
    {
        if( m_match )
            m_break = true;

        return static_cast<CaseDefault<T> &>(*this);
    }

    void Default(const Block &block)
    {
        if( m_break )
            return;

        if( block )
            block();
    }

private:
    void operator =(const CaseBreakDefault &);
};

template <class T>
class CaseDefault : public CaseBreakDefault<T>
{
public:
    explicit CaseDefault(const T &value) :
        CaseBreakDefault(value)
    {
    }

private:
    using CaseBreakDefault::Break;
};

template <class T>
CaseDefault<T> Switch(const T &value)
{
    return CaseDefault<T>(value);
}

Another example

Let’s say you’re making a game where the player controls his character using the keyboard. You might want the keys which control the character to be configured by the player at runtime or come from a configuration file. The following code illustrates how this might look:

C++
// Player position
int playerX, playerY;

// Key codes mapped to physical key codes at runtime
int leftCode, rightCode, upCode, downCode;

void OnKeyPressed(const int code)
{
    Switch(code)
        .Case(leftCode,  [] { --playerX; }).Break()  // move left
        .Case(rightCode, [] { ++playerX; }).Break()  // move right
        .Case(upCode,    [] { --playerY; }).Break()  // move up
        .Case(downCode,  [] { ++playerY; }).Break(); // move down
}

Notes

The performance of such a simple implementation is linear in time. For a relatively small number of case statements, its performance might not matter. For a larger number of case statements, an appropriately tuned data structure should be considered instead.

Improvements

Some ideas that could be experimented with:

  • Break implicitly and fall through explicitly via a Fall function. This would deviate from the native switch statement syntax, but could help in preventing errors.
  • Accept a user-defined binary predicate for performing comparisons. The switch statement could take this predicate as a second parameter, defaulting to using operator equal to, if none were specified.
  • Use a binary search instead of a linear search to improve performance. Of course this would mean that the case expressions would need to be sorted.

Closing

The source code has been tested with Microsoft and Intel C++ compilers.

There is much potential for improvement; if you make changes to the code, improve it, or have some better ideas, I would love to know. I can be reached by email at francisxavierjp [at] gmail [dot] com. Comments and suggestions are always welcome!

 

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication