Using strings in switch statements - where do we stand with C++17?

12.5k views Asked by At

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.

10

There are 10 answers

1
Yakk - Adam Nevraumont On

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.

6
einpoklum On

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.

3
Aganju On

The original reason for the switch statement is that it can be mapped by the compiler to a similar machine operation. For switches with a large amount of cases, this produces very efficient machine code.

For strings, because of the needed comparison, this is not possible, so the implementation would be far less efficient; not any different from if/else/else-if clauses. The C and C++ language family still has the target to allow to produce very efficient machine code without any overhead, so a switch on strings is not something that would be a useful extension - there are more efficient ways to code that, if you really need it to be more efficient. It would also imply to add a 'strcmp' into the language syntax, with all its variations and vagaries - not a good idea.

I doubt that this would be a good extension at any time for any version of C++.

1
PiotrNycz On

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);
}
3
Fabio A. On

Late to the party, here's a solution I came up with some time ago, which completely abides to the requested syntax and works also with c++11.

#include <uberswitch/uberswitch.hpp>

uswitch(my_std_string) {
ucase ("foo"): do_stuff(); break;
ucase ("bar"): do_other_stuff(); break;
default:       just_give_up();
}

The only differences to be noticed are the usage of uswitch in place of switch and ucase in place of case, with the added parenthesis around the value (needed, because that's a macro).

Here's the code: https://github.com/falemagn/uberswitch

3
Ramu On

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

1
Ramu On

Here is another solution. But this version also uses a series of comparisons.

  • 3 assignments (including an Array of all the CASE string pointers)
  • string Comparisons until a match is found
  • increments on an integer until a match is found

DEMO

#include <stdio.h>
#include <string.h>

#define SWITCH(X, ...) \
            char * __switch_case_ ## X ## _decl[] = {__VA_ARGS__}; \
            int __switch_case_ ## X ## _i=0, __switch_case_ ## X ## _size = sizeof(__switch_case_ ## X ## _decl)/sizeof(char*); \
            while (__switch_case_ ## X ## _i < __switch_case_ ## X ## _size && strcmp(X, __switch_case_ ## X ## _decl[__switch_case_ ## X ## _i])){ __switch_case_ ## X ## _i++; } \
            switch (__switch_case_ ## X ## _i)


int main()
{
    char * str = "def";

    SWITCH (str, "abc", "def", "ghi", "jkl")
    {
    case 0:
        printf (str);
        break;
    case 1:
        printf (str);
        break;
    case 2:
        printf (str);
        break;
    case 3:
        printf (str);
        break;
    default:
        printf ("default");
    }

    return 0;
}

OUTPUT:

def
2
smilingthax On

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.

0
alberto On

In C++17, I take advantage of std::find as part of the <algorithm> group. The idea is to keep all case value strings together inside a searchable container (such as std::vector). We will try to locate the string being searched and, then, switch based on the int index found.

So, let's start creating a finder template such as:

template<typename T>
int find_case(std::vector<T> vHaystack, T tNeedle)
{
    int nPos(-1);
    typename std::vector<T>::iterator it =
        std::find(vHaystack.begin(), vHaystack.end(), tNeedle);
    if (it != vHaystack.cend())
        nPos = std::distance(vHaystack.begin(), it);
    return nPos;
}

find_case will return -1 when the needle is not found inside the haystack, or the non-negative index, if the needle is found.

These are usage examples:

std::vector<std::string> v1 { "Hello", "How", "are", "you" };
int n1(find_case(v1, "How"));    // Will return 1
int n2(find_case(v1, "Bye"));    // Will return -1

One advantage of being a template is that we could also work with other types, such as std::wstring.

Now, let's have a look at the switch on the index:

// vCases is the haystack, the vector with all case strings
// strKey is the needle, the value to be searched
switch (int nIndex; nIndex = find_case(vCases, strKey))
{
    case 0:   ...; break;
    case 1:   ...; break;
    case 2:   ...; break;
    ...
    default:
        // User gave a wrong/unexpected key
        if (nIndex < 0)
            std::cout << "Unknown case " << strKey << std::endl;
        // Our list of switch cases is missing one, at least
        else
            std::cerr << "INTERNAL: No case for " << strKey << std::endl;
}

Do not forget to include <vector> and <algorithm> headers.

Out of the scope of this answer, there are much more powerful implementations using std searchers in combination with std::any that will let us have string and integer type cases together under a common switch statement.

1
xenophōn On

If your C++ compiler supports the C++17 standard, it is possible to calculate the hash of a string_view literal with a constexpr function at compile time for switch...case statements.

#include <string_view>
using namespace std::literals::string_view_literals;
#include <string>
using namespace std::literals::string_literals;

//
// The following code ported from Microsoft C++ STL std::hash<std::string_view>
// make it a 'constexpr' and working for string_view literals at compile-time
//

// These FNV-1a utility functions are extremely performance sensitive,
// check examples like that in VSO-653642 before making changes.
#if defined(_WIN64)
static inline constexpr std::size_t __FNV_basis = 14695981039346656037ULL;
static inline constexpr std::size_t __FNV_prime = 1099511628211ULL;
#else
static inline constexpr std::size_t __FNV_basis = 2166136261U;
static inline constexpr std::size_t __FNV_prime = 16777619U;
#endif

static inline constexpr std::size_t __FNV_hash(const std::string_view sv)
{
    std::size_t hash_{ __FNV_basis };
    for (std::size_t i = 0; i < sv.size(); ++i)
    {
        hash_ ^= static_cast<std::size_t>(sv.at(i));
        hash_ *= __FNV_prime;
    }
    return hash_;
}

int main(int argc, char *argv[])
{
    std::string str(argv[0]);

    switch (__FNV_hash(str)) {
    case __FNV_hash("ls"sv):
        break;
    case __FNV_hash("chmod"sv):
        break;
    case __FNV_hash("cp"sv):
        break;
    default:
        break;
    }

    return 0;
}