Using strings in switch statements - where do we stand with C++17?
Every one of us has (probably) had the childhood dream of writing:
switch(my_std_string) {
case "foo": do_stuff(); break;
case "bar": do_other_stuff(); break;
default: just_give_up();
}
but this is not possible, as is explained in the answers to this question from the olden days (2009):
Why the switch statement cannot be applied on strings?
Since then we've seen the advent of C++11, which lets us go as far as:
switch (my_hash::hash(my_std_string)) {
case "foo"_hash: do_stuff(); break;
case "bar"_hash: do_other_stuff(); break;
default: just_give_up();
}
as described in an answer to Compile time string hashing - which is not so bad, although it doesn't actually do exactly what we wanted - there's a chance of collision.
My question is: Has the development of the language since then (mostly C++14 I suppose) affected the way one would write a sort-of-a string case statement? Or simplified the nuts-and-bolts for achieving the above?
Specifically, with the conclusion of the C++17 standard being just around the corner - I'm interested in the answer given what we can assume the standard will contain.
Solution 1:
My proposal is possible with C++14, but with if constexpr
and std::string_view
it is a little esier to write.
First - we need constexpr string - like this one:
template <char... c>
using ConstString = std::integer_sequence<char, c...>;
template <char ...c>
constexpr auto operator ""_cstr ()
{
return ConstString<c...>{};
}
operator ==
is also easier to write with template-less construction of tuple
and with the fact that tuple
has now constexpr operator ==
:
template <char... c1, char ...c2>
constexpr bool operator == (ConstString<c1...>, ConstString<c2...>)
{
if constexpr (sizeof...(c1) == sizeof...(c2)) // c++17 only
{
return tuple{c1...} == tuple{c2...}; // c++17 only
}
else
{
return false;
}
}
Next thing - define switch-case code:
template <typename Callable, typename Key>
class StringSwitchCase;
template <typename Callable, char ...c>
struct StringSwitchCase<Callable, ConstString<c...>>
{
constexpr bool operator == (const std::string_view& str) // c++17 only
{
constexpr char val[] = {c..., '\0'};
return val == str;
}
Callable call;
static constexpr ConstString<c...> key{};
};
template <typename Callable, char ...c>
constexpr auto makeStringSwitchCase(CString<c...>, Callable call)
{
return StringSwitchCase<Callable, ConstString<c...>>{call};
}
Default case would be also needed:
template <typename Callable>
struct StringSwitchDefaultCase
{
constexpr bool operator == (const std::string_view&)
{
return true;
}
Callable call;
};
template <typename Callable>
constexpr auto makeStringSwitchDefaultCase(Callable call)
{
return StringSwitchDefaultCase<Callable>{call};
}
So, the StringSwitch
- actually, it is if () {} else if () {} ... else {}
construction:
template <typename ...Cases>
class StringSwitch
{
public:
StringSwitch(Cases&&... cases) : cases(std::forward<Cases>(cases)...) {}
constexpr auto call(const std::string_view& str)
{
return call<0u>(str);
}
private:
template <std::size_t idx>
constexpr auto call(const std::string_view& str)
{
if constexpr (idx < sizeof...(Cases))
{
if (std::get<idx>(cases) == str)
{
return std::get<idx>(cases).call();
}
return call<idx + 1>(str);
}
else
{
return;
}
}
std::tuple<Cases...> cases;
};
And possible usage:
StringSwitch cstrSwitch(
makeStringSwitchCase(234_cstr,
[] {
cout << "234\n";
}),
makeStringSwitchCase(ConstString<'a', 'b', 'c'>{}, // only C++ standard committee know why I cannot write "abc"_cstr
[] {
cout << "abc\n";
}),
makeStringSwitchDefaultCase([] {
cout << "Default\n";
}));
cstrSwitch.call("abc"s);
Working demo.
I manage to do ConstString in much easier way, basing on this post. Working demo2.
The added part is as follows:
#include <boost/preprocessor/repetition/repeat.hpp>
#include <boost/preprocessor/comma_if.hpp>
#define ELEMENT_OR_NULL(z, n, text) BOOST_PP_COMMA_IF(n) (n < sizeof(text)) ? text[n] : 0
#define CONST_STRING(value) typename ExpandConstString<ConstString<BOOST_PP_REPEAT(20, ELEMENT_OR_NULL, #value)>, \
ConstString<>, sizeof(#value) - 1>::type
template <typename S, typename R, int N>
struct ExpandConstString;
template <char S1, char ...S, char ...R, int N>
struct ExpandConstString<ConstString<S1, S...>, ConstString<R...>, N> :
ExpandConstString<ConstString<S...>, ConstString<R..., S1>, N - 1>
{};
template <char S1, char ...S, char ...R>
struct ExpandConstString<ConstString<S1, S...>, ConstString<R...>, 0>
{
using type = ConstString<R...>;
};
By changing first parameter (20
) in BOOST_PP_REPEAT(20, ELEMENT_OR_NULL, #value)
we can control the maximum possible size of ConstString
- and the usage is as follows:
int main() {
StringSwitch cstrSwitch(
makeStringSwitchCase(CONST_STRING(234){},
[] {
cout << "234\n";
}),
makeStringSwitchCase(CONST_STRING(abc){},
[] {
cout << "abc\n";
}),
makeStringSwitchDefaultCase([] {
cout << "Default\n";
}));
cstrSwitch.call("abc"s);
}
Solution 2:
It would be easy-ish to write
switcher(expr)->*
caser(case0)->*[&]{
}->*
caser(case1)->*[&]{
};
to build a statically sized hash table of case0
through caseN
, populate it dynamically, test for collisions with ==
, do the lookup via expr
, and run the corresponding lambda.
Even caser(case3)->*caser(case4)->*lambda
and ->*fallthrough
could be supported.
I do not see a compelling need.
I see no advantage to writing this in C++17 either.
Solution 3:
Since C++11 you can use smilingthax/cttrie (cf. C/C++: switch for non-integers - esp. Update 2016):
#include "cttrie.h"
...
const char *str = ...;
TRIE(str)
std::cout << "Not found\n";
CASE("abc")
std::cout << "Found abc\n";
CASE("bcd")
std::cout << "Found bcd\n";
ENDTRIE;
Internally, a Trie is created at compile time and stored as a type. At runtime it is traversed according to str
. The code blocks are wrapped in lambdas and executed at the corresponding leafs.
Solution 4:
Here is a simple solution for simulating switch case in C/C++.
UPDATE: Including continue version. Earlier version cannot use continue statement within a loop. Regular switch-case block can perform continue, as expected, when used in a loop. But since we use for loop in our SWITCH-CASE macros, continue just brings out of the SWITCH-CASE block but not out of the loop, in which it is being used.
Here are the macros to be used:
#ifndef SWITCH_CASE_INIT
#define SWITCH_CASE_INIT
char __switch_continue__;
#define SWITCH(X) __switch_continue__=0; \
for (char* __switch_p__=X, __switch_next__=1; __switch_p__!=0 ; __switch_next__=2) { \
if (__switch_next__==2) { __switch_continue__=1; break;
#define CASE(X) } if (!__switch_next__ || !(__switch_next__ = strcmp(__switch_p__, X))) {
#define DEFAULT } {
#define END __switch_p__=0; }}
#define CONTINUE __switch_p__=0; }} if (__switch_continue__) { continue; }
#endif
EXAMPLE: SWITCH-CASE with continue
EXECUTE
If the SWITCH block is used in a loop and we happen to use continue within the SWITCH, we need to end the SWITCH with CONTINUE (rather than END)
#include <stdio.h>
#include <string.h>
#ifndef SWITCH_CASE_INIT
#define SWITCH_CASE_INIT
char __switch_continue__;
#define SWITCH(X) __switch_continue__=0; \
for (char* __switch_p__=X, __switch_next__=1; __switch_p__!=0 ; __switch_next__=2) { \
if (__switch_next__==2) { __switch_continue__=1; break;
#define CASE(X) } if (!__switch_next__ || !(__switch_next__ = strcmp(__switch_p__, X))) {
#define DEFAULT } {
#define END __switch_p__=0; }}
#define CONTINUE __switch_p__=0; }} if (__switch_continue__) { continue; }
#endif
int main()
{
char* str = "def";
char* str1 = "xyz";
while (1) {
SWITCH (str)
CASE ("abc")
printf ("in abc\n");
break;
CASE ("def")
printf("in def (continuing)\n");
str = "ghi";
continue; // <== Notice: Usage of continue (back to enclosing while loop)
CASE ("ghi") // <== Notice: break; not given for this case. Rolls over to subsequent CASEs through DEFAULT
printf ("in ghi (not breaking)\n");
DEFAULT
printf("in DEFAULT\n");
CONTINUE // <== Notice: Need to end the SWITCH with CONTINUE
break; // break while(1)
}
}
OUTPUT:
in def (continuing)
in ghi (not breaking)
in DEFAULT
Need to use SWITCH..CASE..CONTINUE inside a loop (that too if continue is required within the switch)
Need to use SWITCH..CASE..END by default
-
Can use reverse string comparison. Like
SWITCH ("abc") CASE(str1) END
This kind of comparison can open a whole lot of comparison options and avoid clumsy if-else chains. String comparison cannot be made without character-by-character comparison and so cannot avoid if-else chains. At least code looks cute with SWITCH-CASE. But the bottleneck is it uses
- 3 extra variables
- 5 extra assignments and
- 1 extra (bool) comparison for each CASE
So itz on developers' discretion of opting between if-else to SWITCH-CASE
Solution 5:
A minor modification of @PiotrNycz's interesting answer, to make the syntax a bit more similar to the 'naive' switch, allows us to write this:
switch_(my_std_string,
case_(234_cstr, [] {
std::cout << "do stuff with the string \"234\" \n";
}),
case_(ConstString<'a', 'b', 'c'> { }, [] {
std::cout << "do other stuff with the string \"abc\"\n";
}),
default_( [] {
std::cout << "just give up.\n";
})
The full implementation:
#include <iostream>
#include <array>
#include <tuple>
#include <string>
#include <type_traits>
#include <utility>
template<char ... c>
using ConstString = std::integer_sequence<char, c...>;
template <char ...c>
constexpr auto operator ""_cstr ()
{
return ConstString<c...> {};
}
template<char ... c1, char ...c2>
constexpr bool operator == (ConstString<c1...>, ConstString<c2...>)
{
if constexpr (sizeof...(c1) == sizeof...(c2)) {
return std::tuple {c1...} == std::tuple {c2...};
}
else { return false; }
}
template<typename Callable, typename Key>
class SwitchCase;
template<typename Callable, char ...c>
struct SwitchCase<Callable, ConstString<c...>> {
constexpr bool operator == (const std::string_view& str) {
constexpr char val[] = { c..., '\0' };
return val == str;
}
const ConstString<c...> key;
Callable call;
};
template<typename Callable, char ...c>
constexpr auto case_(ConstString<c...> key, Callable call)
{
return SwitchCase<Callable, ConstString<c...>> { key, call };
}
template<typename Callable>
struct SwitchDefaultCase {
constexpr bool operator == (const std::string_view&) { return true; }
Callable call;
};
template<typename Callable>
constexpr auto default_(Callable call)
{
return SwitchDefaultCase<Callable> { call };
}
template<typename ...Cases>
class switch_ {
public:
// I thought of leaving this enabled, but it clashes with the second ctor somehow
// switch_(Cases&&... cases) : cases(std::forward<Cases>(cases)...) {}
constexpr auto call(const std::string_view& str) {
return call<0u>(str);
}
switch_(const std::string_view&& str, Cases&&... cases) :
cases(std::forward<Cases>(cases)...) {
call<0u>(str);
}
private:
template<std::size_t idx>
constexpr auto call(const std::string_view& str) {
if constexpr (idx < sizeof...(Cases)) {
if (std::get<idx>(cases) == str) {
return std::get<idx>(cases).call();
}
return call<idx + 1>(str);
}
else { return; }
}
std::tuple<Cases...> cases;
};
int main() {
std::string my_std_string("abc");
std::cout << "What is \"" << my_std_string << "\"?\n";
switch_(my_std_string,
case_(234_cstr, [] {
std::cout << "do stuff\n";
}),
case_(ConstString<'a', 'b', 'c'> { }, [] {
std::cout << "do other stuff\n";
}),
default_( [] {
std::cout << "just give up\n";
})
);
}
And a similar working demo. Now what we would really need is constructing ConstStrings from "abcd" -type literals.