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.