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:
switch statementint, char, enum ... )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.
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:
switch( num )
{
case 1 ... 9: break;
case 10 ... 99: break;
};
| switch( ch )
{
case 'A' ... 'Z': break;
case 'a' ... 'z': break;
};
|
switch statement: 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")
}
|
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:
eswitch as close as possible to the switch statement. Compare: switch( num )
{
case 1: {...} break;
case 2: {...} break;
default: {...} break;
};
|
switch statement such as default fallthrough.switch statement has explicit break and implicit fallthrough.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. I worked so hard in order to be as close as possible to the performance of the switch statement. 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.
| eswitch | if statement |
|---|---|
any_from helper function | |
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 ofthose objects is unified, whereas in raw C++ usage is different. | |
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 | |
if( auto * c = dynamic_cast< circle* >( base_ptr ) )
{
c->draw();
}
else if( auto * s = dynamic_cast< square* >( base_ptr ) )
{
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 | |
smatch match;
{
return "word";
}
{
return "number";
}
| |
Match and withdraw values from std::regex | |
smatch match;
{
return match[1];
}
{
return match[1];
}
| |
Match for individual entries in std::pair | |
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 | |
if ( num > 1 && num < 10 ) { ... }
else if( num >= 11 && num <= 20 ) { ... }
| |
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.
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)
Should work on all major compilers which support C++20. I personally tested on the following:
In all code examples, I omit the namespace prefixes for names in the eswitch_v5 and std namespaces.
This section contains all the details, which user need to know in order to use this library successfully.
| 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 |
1. Full declaration
2. Omitted parameter: same as lambda without parameter
3. Omitted setting options: default option( break ) will be used
4. Omitted 'Default': no fallback
Explanation
| Name | Details | ||||||
|---|---|---|---|---|---|---|---|
| __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-onecorrespondance with arguments in eswitch. Consider following code: Possible usages: ________________________________________________________________
Case( smth1, smth2, ... ) (2)
________________________________________________________________
________________________________________________________________
________________________________________________________________
________________________________________________________________
| ||||||
| |||||||
__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 thereturned value wrapped into std::optional from custom extensions.
| ||||||
Optional: __options__ |
|
Case behavior for any types. We will implement a custom extension and I walk you through all steps and details.operators such as ==, !=, >, < and so on.operator== for primitives is forbidden.double_value in our overloaded operator== : double_value allows for compiler through name lookup to find our custom operator==. operator (unless this operator will be defined before #include <eswitch_v5.hpp>, only then intermediate class won't be needed).operator and transferred to Case as input parameter, then the value should be wrapped into std::optional.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.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: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.