我们每个人(可能)都有童年的写作梦想:
switch(my_std_string) {
case "foo": do_stuff(); break;
case "bar": do_other_stuff(); break;
default: just_give_up();
}
但这是不可能的,正如古代(2009年)对这个问题的答案所解释的那样:
Why switch statement cannot be applied on strings?
从那时起,我们已经看到了C ++ 11的出现,它让我们走得更远:
switch (my_hash::hash(my_std_string)) {
case "foo"_hash: do_stuff(); break;
case "bar"_hash: do_other_stuff(); break;
default: just_give_up();
}
正如answer对Compile time string hashing所描述的那样 - 虽然它实际上并没有完全符合我们的要求 - 但它有可能发生碰撞。
我的问题是:从那时起语言的发展(我认为主要是C ++ 14)是否影响了编写一个字符串case语句的方式?或简化实现上述目的的螺母和螺栓?
具体来说,随着C++17 standard的结论是just around the corner - 我对我们可以假设标准将包含的答案感兴趣。
注意:这不是关于使用switch语句的优点的讨论,也不是关于meta的线程的讨论。我问的是一个内容丰富的问题,所以请在此基础上回答/ up / downvote。
这很容易写
switcher(expr)->*
caser(case0)->*[&]{
}->*
caser(case1)->*[&]{
};
通过case0
构建一个静态大小的caseN
哈希表,动态填充它,测试与==
的冲突,通过expr
进行查找,并运行相应的lambda。
甚至可以支持caser(case3)->*caser(case4)->*lambda
和->*fallthrough
。
我没有看到迫切需要。
我认为在C ++ 17中编写它也没有任何优势。
我的建议可以用C ++ 14来实现,但是使用if constexpr
和std::string_view
,它的编写速度有点小。
首先 - 我们需要constexpr字符串 - 就像这样:
template <char... c>
using ConstString = std::integer_sequence<char, c...>;
template <char ...c>
constexpr auto operator ""_cstr ()
{
return ConstString<c...>{};
}
使用operator ==
的无模板构造以及tuple
现在有了constexpr tuple
的事实,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;
}
}
接下来 - 定义switch-case代码:
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};
}
还需要默认情况:
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};
}
所以,StringSwitch
- 实际上,它是if () {} else if () {} ... else {}
结构:
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;
};
可能的用法:
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);
工作demo。
我设法以更容易的方式做ConstString,基于这个post。工作demo2。
增加的部分如下:
#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...>;
};
通过更改20
中的第一个参数(BOOST_PP_REPEAT(20, ELEMENT_OR_NULL, #value)
),我们可以控制ConstString
的最大可能大小 - 用法如下:
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);
}
对@ PiotrNycz有趣的回答进行了一些修改,使语法与'naive'开关更相似,允许我们写这个:
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";
})
全面实施:
#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";
})
);
}
和类似的working demo。现在我们真正需要的是从“abcd”类型的文字构造ConstStrings。
这是一个用于在C / C ++中模拟开关盒的简单解决方案。
更新:包括继续版本。早期版本不能在循环内使用continue语句。当在循环中使用时,常规开关盒块可以按预期继续执行。但是因为我们在SWITCH-CASE宏中使用for循环,所以继续只是从SWITCH-CASE块中取出,而不是在使用它的循环之外。
以下是要使用的宏:
#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
示例:SWITCH-CASE with continue
如果在循环中使用SWITCH块并且我们碰巧在SWITCH中使用continue,我们需要使用CONTINUE结束SWITCH(而不是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
这种比较可以打开很多比较选项,避免笨拙的if-else链。如果没有逐字符比较,就无法进行字符串比较,因此无法避免if-else链。使用SWITCH-CASE至少代码看起来很可爱。但它的瓶颈在于它的用途
因此,开发人员可以自行决定选择if-else和SWITCH-CASE
从C ++ 11开始,您可以使用smilingthax/cttrie(参见C/C++: switch for non-integers - 尤其是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;
在内部,Trie在编译时创建并存储为类型。在运行时,它根据str
遍历。代码块包含在lambdas中并在相应的叶子上执行。
这是另一种解决方案。但是这个版本也使用了一系列的比较。
#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
switch
语句的最初原因是它可以由编译器映射到类似的机器操作。对于具有大量案例的交换机,这会产生非常高效的机器代码。
对于字符串,由于需要进行比较,这是不可能的,因此实现效率会低得多;与if / else / else-if子句没有任何不同。 C和C ++语言系列仍然有一个目标,允许生成非常高效的机器代码,没有任何开销,因此切换字符串不是一个有用的扩展 - 如果你真的需要,有更有效的方法来编码它更有效率。它还意味着在语言语法中添加一个'strcmp',包括它的所有变化和变幻莫测 - 这不是一个好主意。
我怀疑这对任何版本的C ++来说都是一个很好的扩展。