问题
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 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.
Note: This is not a discussion on the merit of using switch statements, nor a thread on meta. I'm asking an informative question, so please answer/up/downvote based on that.
回答1:
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.
回答2:
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:
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.
回答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
回答5:
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.
回答6:
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
回答7:
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++.
来源:https://stackoverflow.com/questions/41337256/using-strings-in-switch-statements-where-do-we-stand-with-c17