16

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.

23
  • 15
    I can't say I've ever had such a dream. The hash example you give has a non-zero probability of giving a false hit (i.e. there is a non-zero probability of two non-equal strings having an equal hash). Given the origins of switch (i.e. exploiting machine instructions that allowed constructing a jump table based on integral values) I would be surprised if any version of C++ ever supported non-integral switch/case. Commented Dec 26, 2016 at 23:33
  • 2
    Agree, with @Peter, the hash solution is NOT formally correct... unless -maybe- an additional check is made to verify the equality, so no clear benefit. OTOH, having a switch for strings in the language, what would be the benefit, really? Commented Dec 26, 2016 at 23:37
  • 3
    @OP if one needs really a switch, a possible solution would be to use some static std::map<string, int> and make the switch on the associated int. This solution would be formally correct and intuitive. Commented Dec 26, 2016 at 23:41
  • 2
    @RadLexus still, the solution is not formally correct. Commented Dec 27, 2016 at 0:00
  • 8
    @RadLexus Your link shows the exact opposite. Your link shows that the chance of hash collisions was considered, and that's why after checking the hash code, the string is compared to the expected value specifically so that hash collisions do not form a problem. Commented Dec 27, 2016 at 1:27

10 Answers 10

5

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);
}
Sign up to request clarification or add additional context in comments.

1 Comment

Hmm, nice. I'm going to tweak this a little to make it a bit more like the "naive" syntax.
4

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.

1 Comment

Is there any sample code to show how it can be implemented? how this solution is compared with this solution: hbfs.wordpress.com/2017/01/10/…
4

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.

2 Comments

So, this looks much nicer than the other macro solution, but will this work for non-char* strings? Which are not known at compile time?
You can also use std::string str = ...; – in fact every class that has .c_str() and .size() is supposed to work (minor bug in the stringview implementation is now fixed).
2

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

3 Comments

1. How is this different from chaining if-then-else's? 2. Why is the for loop necessary?
Thank you for this code snippet, which might provide some limited, immediate help. A proper explanation would greatly improve its long-term value by showing why this is a good solution to the problem, and would make it more useful to future readers with other, similar questions. Please edit your answer to add some explanation, including the assumptions you've made.
@einpoklum 1). Well if-then-else can be chained for ordinary switch-case as well, but why is the need of switch-case? Itz just for simplicity, wherein retyping same variable name with == and if-else makes the code clumsy. By the way, this approach can be used for reverse string switch as well like: SWITCH ("abc") CASE (str1) END which is not possible in switch-case; means a variable cannot be used within a case clause. 2). Yes for loop is needed to simulate break; statement and avoid roll over to subsequent CASEs through DEFAULT.
1

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.

6 Comments

what is 234_cstr in this code? Can I use a case like this case_("My test string")?
@mans: It's basically "234". Edited to clarify and see also PiotrNycz's answer.
Thanks for quick reply. So how can I use it for checking "my test string" can I use it, please note that it has a space in string.
@mans: You can see how to use this mechanism in the main() function. It doesn't matter whether the string has spaces or not.
Can I run it in VS 2015? or does it need VS2017 ?
|
1

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;
}

1 Comment

On second thought, and even though the hash is kind of risky, I'll still upvote.
0

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++.

3 Comments

You're making several claims here which I don't agree with, but regardless of that - you're not answering the question. I specifically said I'm not asking about the merits of doing this, just about the extent to which it's close to being possible.
How it's implemented under the hood is irrelevant; it's the clarity of the source code that's of interest here, particularly as numerous other languages (C#, Java, VB) support switching on string values. docs.oracle.com/javase/7/docs/technotes/guides/language/… specifically mentions that "The Java compiler generates generally more efficient bytecode from switch statements that use String objects than from chained if-then-else statements", so I don't see any reason why C++ could not also provide this.
What are the more efficient ways to code that?
0

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

1 Comment

Well, it's a solution with macros, which I dislike; but regardless of that - what would you say are the benefits here relative to your other suggestion?
0

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 Comment

but I don't see the string near the case :-(
-1

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 Comments

Replacing "case" with a macro is absolutely not acceptable. Why don't you use case_ or ucase (for uber-case) etc? Also, how is your solution different than @Ramu's ?
I used case because syntax highlighters like it much better than any other name I could give it, but if you've got such a strong opinion about it, changing the name of the macro is easier than writing a comment on Stack Overflow, I believe? :)
This solution is different than @Ramu's in that... both the syntax and the implementations and functionalities are different, which seemed quite obvious to me?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.