eswitch-v5
Advanced counterpart for switch statement in C++
User Manual

View on github

Overview


eswitch_v5 is a library, which is an improved version of the switch statement in C++. The main idea behind this library is to be able to overcome switch statement limitations, such as:

  • one argument per the switch statement
  • the argument restricted to only integral types( int, char, enum ... )
  • impossibility of composing complex conditions, as opposed to other statements and loops in C++

eswitch_v5 supports any number of arguments and almost without restriction on their type, provided that the type is comparable( i.e. has operator==, operator!= and so on ). Additionally, my library has gone beyond overcoming the limitations of the switch statement e.g. I have introduced more expressive way for matching and withdrawing values from std::any, std::variant, polymorphic types and std::regex.

Motivation


Let's start with a little bit of backstory. As you know in C++, the switch statement is quite limited( compared with other statements ) and there are few reasons for this, which are mainly related to optimization. Compiler can use jump table or binary search to optimize the switch statement, but nothing comes without a price as such optimizations are possible only for compile-time data. In C++98, only values of primitives like int, char, enum and bool can be known at compile time. That's why the switch statement was restricted to primitive-only. However, in C++11, the new keyword constexpr was introduced. Which opened the doors for compile-time custom types. So now it has become possible to overcome the primitive-only limitation. There was also the proposal in this regard. Unfortunately, for whatever reason, the author decided to bail out, even though the feedback from the committee was positive. Nonetheless, even if this proposal was accepted in the C++ standard, the switch statement still wouldn't be as powerful as the if statement.

Let us now carefully examine the if statement. It can be used for various data types that are known at compile time and at runtime. In addition to this, it allows to compose and test complex conditions. Wait wait wait, if the if statement can work with data known at compile-time, then why can't compiler optimize this statement just like the switch statement? In theory, it can. Let me even say that there is a compiler that is capable of doing so and this is clang.

For example:

int foo(int num)
{
if ( num == 10 ) return 1;
else if( num == 13 ) return 2;
else if( num == 11 ) return 3;
else if( num == 19 ) return 4;
else if( num == 12 ) return 5;
else return 6;
}
int bar(int num) {
switch( num )
{
case 19: return 4;
case 10: return 1;
case 13: return 2;
case 11: return 3;
case 12: return 5;
default: return 6;
};
}

foo(int): # @foo(int)
add edi, -10
cmp edi, 9
ja .LBB0_2
movsxd rax, edi
mov eax, dword ptr [4*rax + .Lswitch.table.bar(int)]
ret
.LBB0_2:
mov eax, 6
ret
bar(int): # @bar(int)
add edi, -10
cmp edi, 9
ja .LBB1_2
movsxd rax, edi
mov eax, dword ptr [4*rax + .Lswitch.table.bar(int)]
ret
.LBB1_2:
mov eax, 6
ret
.Lswitch.table.bar(int):
.long 1 # 0x1
.long 3 # 0x3
.long 5 # 0x5
.long 2 # 0x2
.long 6 # 0x6
.long 6 # 0x6
.long 6 # 0x6
.long 6 # 0x6
.long 6 # 0x6
.long 4 # 0x4

If those optimizations are possible, then it means that the limitations of the switch statement make no sense and never had, given that even 10 years old clang-3.0 could do such optimizations. Clang has shown us that similar optimizations can be applied to both of these statements. However, the if statement can also work with runtime data, whereas the switch statement cannot.

And this situation is unfortunate for me and others. The Internet is filled with questions(over million views) from people who are trying to find an insight or a workaround:

Moreover, I can see all over the place many attempts to overcome those limitations in some way or another:

  • Compilers such as clang and gcc implemented non-standard extension for matching case ranges:
    switch( num )
    {
    case 1 ... 9: break;
    case 10 ... 99: break;
    };

    switch( ch )
    {
    case 'A' ... 'Z': break;
    case 'a' ... 'z': break;
    };

  • LLVM implemented StringSwitch:
    Color color = StringSwitch<Color>(argv[i])
    .Case("red", Red)
    .Case("orange", Orange)
    .Case("yellow", Yellow)
    .Case("green", Green)
    .Case("blue", Blue)
    .Case("indigo", Indigo)
    .Cases("violet", "purple", Violet)
    .Default(UnknownColor);
  • Apple created the programming language Swift with advanced the switch statement:
    switch num
    {
    case 1..<10:
    print("range [1,10)")
    case 10..<100:
    print("range [10,100)")
    case 100..<1000:
    print("range [100,1000)")
    default:
    print("unknown range")
    }

    switch someCharacter
    {
    case "a", "e", "i", "o", "u":
    print("a vowel")
    case "b", "c", "d", "f", "g", "h", "j",
    "k", "l", "m", "n", "p", "q", "r",
    "s", "t", "v", "w", "x", "y", "z":
    print("a consonant")
    default:
    print("neither a vowel nor a consonant")
    }

    switch somePoint
    {
    case (0, 0): print("at the origin")
    case (_, 0): print("on the x-axis")
    case (0, _):
    print("on the y-axis")
    case (-2...2, -2...2):
    print("inside the box")
    default:
    print("outside of the box")
    }

Apparently, the switch statement also can be as powerful as the if statement. This insight became my motivation to start this project. Where I tried:

  1. Address all the limitations
  2. Leave the syntax of eswitch as close as possible to the switch statement. Compare:
    switch( num )
    {
    case 1: {...} break;
    case 2: {...} break;
    default: {...} break;
    };

    eswitch( num )
    (
    Case( 1 ) {...},
    Case( 2 ) {...},
    Default {...}
    );

    Pretty close, isn't it? Except the places where I've been either limited by the language or intentionally tried to avoid one particular behavior of the switch statement such as default fallthrough.

    Note
    In C++ switch statement has explicit break and implicit fallthrough.

    This behavior is considered error-prone. Since developers occasionally forget to use break. Thus, in my implementation, I reversed this concept i.e. eswitch has an implicit break and an explicit fallthrough. Compare:
    switch( num )
    {
    case 1: {...}
    case 2: {...} break;
    default: {...} break;
    };

    eswitch( num )
    (
    Case( 1 ){...} ^ fallthrough_,
    Case( 2 ){...},
    Default {...}
    );

  3. And last but not least, another my priority was the performance of eswitch. I worked so hard in order to be as close as possible to the performance of the switch statement.

Comparison


Unfortunately, the full comparison of eswitch vs the switch statement isn't possible due to the limitations of the latter. Thus I'm going to make a comparison with the if statement instead.

eswitchif statement
any_from helper function
eswitch( file_extension )
(
Case( any_from( "cpp", "c++", "cxx", "cc", "C" ) )
{
return "source";
},
Case( any_from( "hpp", "h++", "hxx", "hh", "h", "H" ) )
{
return "header";
}
);

if( file_extension == "cpp" ||
file_extension == "c++" ||
file_extension == "cxx" ||
file_extension == "cc" ||
file_extension == "C" )
{
return "source";
}
else if(
file_extension == "hpp" ||
file_extension == "h++" ||
file_extension == "hxx" ||
file_extension == "hh" ||
file_extension == "h" ||
file_extension == "H" )
{
return "header";
}

Match and withdraw value from std::any and std::variant - in terms of this library the usage for matching type in either of
those objects is unified, whereas in raw C++ usage is different.
eswitch( any_or_variant_obj )
(
Case( is< int >{} )( const int value )
{
...
},
Case( is< string >{} )( const string & value )
{
...
}
);

std::any any_obj = ...;
if( auto * value = any_cast< int >( &any_obj ) )
{
...
}
else if( auto * value = any_cast< string >( &any_obj ) )
{
...
}

std::variant< int, string > variant_obj = ...;
if( auto * value = get_if< int >( &variant_obj ) )
{
...
}
else if( auto * value = get_if< string >( &variant_obj ) )
{
...
}

Match for polymorphic types
eswitch( base_ptr )
(
Case( is< circle >{} )( circle * c )
{
c->draw();
},
Case( is< square >{} )( square * s )
{
s->draw();
}
);

if( auto * c = dynamic_cast< circle* >( base_ptr ) )
{
c->draw();
}
else if( auto * s = dynamic_cast< square* >( base_ptr ) )
{
s->draw();
}

eswitch( base_ref )
(
Case( is< circle >{} )( circle & c )
{
c.draw();
},
Case( is< square >{} )( square & s )
{
s.draw();
}
);

try
{
auto & c = dynamic_cast< circle& >( base_ptr );
c.draw();
return;
}
catch( const bad_cast & ) { ... }
try
{
auto & s = dynamic_cast< square& >( base_ptr );
s.draw();
return;
}
catch( const bad_cast & ) { ... }

Match for std::regex
eswitch( text )
(
Case( R"(\w*)"_r ) { return "word"; },
Case( R"(\d*)"_r ) { return "number"; }
);

smatch match;
if( regex_match( text, match, regex( R"(\w*)" ) ) )
{
return "word";
}
else if( regex_match( text, match, regex( R"(\d*)" ) ) )
{
return "number";
}

Match and withdraw values from std::regex
eswitch( text )
(
Case( R"((\w*))"_r )( vector< string > && match )
{
return match[1];
}
Case( R"((\d*))"_r )( vector< string > && match )
{
return match[1];
}
);

smatch match;
if( regex_match( text, match, regex( R"((\w*))" ) ) )
{
return match[1];
}
else if( regex_match( text, match, regex( R"((\d*))" ) ) )
{
return match[1];
}

Match for individual entries in std::pair
eswitch( make_pair( 10, string{"Text"} ) )
(
Case( 10, "Text" ){ ... }
);

auto pr = make_pair( 10, string{"Text"} );
if( pr.first == 10 && pr.second == "Text" ) { ... }

Match for individual entries in std::tuple
eswitch( make_tuple( 1, 0, 0, 1 ) )
(
Case( 1, 0, 0, 1 ) { return 9; },
Case( 1, 1, 1, 1 ) { return 15; }
);

auto tup = make_tuple( 1, 0, 0, 1 );
auto [ x, y, z, k ] = tup;
if( x == 1 && y == 0 && z == 0 && k == 1 )
{
return 9;
}
else if( x == 1 && y == 1 && z == 1 && k == 1 )
{
return 15;
}

Check in range
eswitch( p1 )
(
Case( _1.between( 1, 10 ) ) {...},
Case( _1.within( 11, 20 ) ) {...}
);

if ( num > 1 && num < 10 ) { ... }
else if( num >= 11 && num <= 20 ) { ... }

Installation


It is a header-only library. On top of that entire library was implemented within a single file, thus you can get that file from the repository and just include it.

License


This code is distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

Supported Compilers


Should work on all major compilers which support C++20. I personally tested on the following:

  • clang++-11 (or later)
  • g++-10.2 (or later)
  • Visual Studio 2019 - isn't supported for now, just because familiar template syntax for generic lambdas from C++20 wasn't implemented by Microsoft compiler. I reported about this issue, right now it is under investigation.

Conventions used in this Document


In all code examples, I omit the namespace prefixes for names in the eswitch_v5 and std namespaces.

Implementation Details


This section contains all the details, which user need to know in order to use this library successfully.

Keywords


Name Description
eswitch accepts list of arguments
Case accepts condition to check and body next to it will be executed if condition matched
Default body next to it will be executed if nothing else matched
fallthrough_ next body will be executed without checking its condition
any_from accepts values to choose from
is<sometype> used within Case for matching types like std::any, std::variant and polymorphic types match
_r user defined literal for std::regex

Syntax


1. Full declaration

eswitch( __arguments__ )
(
Case( __conditions__ )( __param__ ) { __body__ } ^ __options__,
Default { __body__ }
);

2. Omitted parameter: same as lambda without parameter

eswitch( __arguments__ )
(
Case( __conditions__ ) { __body__ } ^ __options__,
Default { __body__ }
);

3. Omitted setting options: default option( break ) will be used

eswitch( __arguments__ )
(
Case( __conditions__ ) { __body__ },
Default { __body__ }
);

4. Omitted 'Default': no fallback

eswitch( __arguments__ )
(
Case( __conditions__ ) { __body__ }
);

Explanation

NameDetails
__arguments__
The list of arguments( i.e. arg_1, arg_2, ..., arg_n )
__conditions__
It is a lazy expression, where indexes _1, _2, ... represent one-to-one
correspondance with arguments in eswitch. Consider following code:
eswitch(arg_1, arg_2, ..., arg_n)
(
Case( _1 == smth_1 || _2 == smth_2 || ... ) {...}
);
// _1 refer to arg_1
// _2 refer to arg_2
// ...

Possible usages:

Case( _1 == smth1 && _2 == smth2 && ... ) (1)
________________________________________________________________
Case( smth1, smth2, ... ) (2)
________________________________________________________________
Case( _1 == any_from( smth1, smth2, ... ) ) (3)
________________________________________________________________
Case( any_from( smth1, smth2, ... ) ) (4)
________________________________________________________________
Case( ( pred1, _1 ) && ( pred2, _2 ) && ... ) (5)
________________________________________________________________

  1. Match in order
  2. Same as the 1st one, but less verbose
  3. Match via any_from
  4. Same as the 3rd one, but less verbose
  5. Match via predicate
__body__
Function body
Optional:
__param__
Correspond to withdrawn value from: std::any, std::variant,
polymorphic match or std::regex match. But also it corresponds to the
returned value wrapped into std::optional from custom extensions.
Note
Keyword auto here is forbidden,
i.e. type of __param__ should be specified explicitly.
Optional:
__options__
-following Case's won't be executed
fallthrough_execute body of the following Case
likely_messing with branch predictions
( will be introduced in future )

How to write Custom extensions?


This guide deals with how to define the custom Case behavior for any types. We will implement a custom extension and I walk you through all steps and details.
Note
The only things which we can be customized are all the comparison operators such as ==, !=, >, < and so on.
Let's begin:
As you know, comparing floating points is a tricky, for example, the following code fails due to inaccurate precision:
eswitch( 2 - 0.7000000001 )
(
Case( 1.3 )
{
...
},
{
assert( false );
}
);
So the standard way of comparing floating points doesn't work properly. On top of that, simple overloading of operator== for primitives is forbidden.
bool operator==( const double d1, const double d1 )
{
...
}
/// Compilation ERROR: overloaded 'operator==' must have at least one parameter of class or enumeration type
To overcome this, we must use an intermediate class that will hold our value. Like this: 
struct double_value
{
double value;
};
Then we can use double_value in our overloaded operator== :
bool operator==( const double d1, const double_value d2 )
{
return fabs( d1 - d2.value ) < __FLT_EPSILON__;
}
Now code below will work as we desired since using double_value allows for compiler through name lookup to find our custom operator==.
eswitch( 2 - 0.7000000001 )
(
Case( double_value{ 1.3 } )
{
...
},
{
assert( false );
}
);
Full example: Floating point comparison
Note
  • An intermediate class should be used all the time (not only for primitives due to compiler restrictions), otherwise it won't be possible for compiler to find custom operator (unless this operator will be defined before #include <eswitch_v5.hpp>, only then intermediate class won't be needed).
  • Also the example below demonstrate if a certain value should be returned from custom operator and transferred to Case as input parameter, then the value should be wrapped into std::optional.
Example: Value and Type transferring

Rationalities


  • Using macroses for Case and Default allowed me to be as close as possible to the switch statement regarding syntax, otherwise there was no way to hide verbose lambda declaration.
  • I can use compile-time hashing in order to use string within the switch statement. Yes you can. But there are several downsides. First of all, you need to implement it yourself since there is no standard implementation. Second of all, you need to deal with possible collisions. After all those steps you will solve just one limitation, it still won't be as powerful as the if statement, nor my implementation. Which is much more advance since it allows:
    • match for any types,
    • compose complex conditions,
    • write extensions and
    • eswitch can work at compile-time as long as the provided data known at compile-time, but there is one catch, now it is not working and the reason is that std::reference_wrapper isn't constexpr althought it must be since C++20.
std::make_tuple
T make_tuple(T... args)
eswitch_v5::_1
constexpr Index_< 0 > _1
Definition: eswitch_v5.hpp:174
std::fabs
T fabs(T... args)
eswitch_v5::fallthrough_
static constexpr Fallthrough fallthrough_
Indicates fall through from previous Case without testing condition of following Case.
Definition: eswitch_v5.hpp:1016
std::regex_match
T regex_match(T... args)
std::any
eswitch_v5::any_from
static constexpr auto any_from(Args &&... args)
Helper function which allows to find if something is within the list( passed arguments ).
Definition: eswitch_v5.hpp:991
Default
#define Default
Declares body which will be executed in case of no other matches.
Definition: eswitch_v5.hpp:1029
Case
#define Case(...)
Accepts conditions of various forms.
Definition: eswitch_v5.hpp:1026
eswitch_v5::eswitch
static constexpr auto eswitch(Ts &&... ts)
This function is responsible for passing arguments for class eswitch_impl with overloaded operator(),...
Definition: eswitch_v5.hpp:838
std::make_pair
T make_pair(T... args)
eswitch_v5::_
static constexpr extension::any _
Used in Case to match for any input.
Definition: eswitch_v5.hpp:1019
eswitch_v5::extension::range
range
Definition: eswitch_v5.hpp:67
eswitch_v5::operator==
static auto operator==(const std::string &tuple_entry, const regexter &value)
CaseModule to support for matching and withdrawing of values for and from regular expression.
Definition: eswitch_v5.hpp:413
eswitch_v5::_2
constexpr Index_< 1 > _2
Definition: eswitch_v5.hpp:175
std::variant