在switch语句中使用字符串-我们在C++17中处于什么位置?

dwbf0jvd  于 2023-05-19  发布在  其他
关注(0)|答案(9)|浏览(135)

我们每个人都有(可能)童年的写作梦想:

switch(my_std_string) {
case "foo":  do_stuff(); break;
case "bar":  do_other_stuff(); break;
default:     just_give_up();
}

但这是不可能的,正如《奥登时代》(2009年)对这个问题的回答所解释的那样:
Why the 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();
}

正如answerCompile time string hashing中所描述的-这并不那么糟糕,尽管它实际上并没有完全按照我们的要求做-有可能发生冲突。
我的问题是:从那时起,语言的发展(我想主要是C++14)影响了人们编写某种字符串case语句的方式吗?或者简化了实现上述目标的螺母和螺栓?
具体来说,随着C++17 standard的结论是just around the corner-我感兴趣的答案,我们可以假设标准将包含什么。

fdbelqdn

fdbelqdn1#

我的建议在C++14上是可行的,但是在if constexprstd::string_view上写起来稍微容易一些。
首先-我们需要constexpr字符串-像这样:

template <char... c>
using ConstString = std::integer_sequence<char, c...>;

template <char ...c>
constexpr auto operator ""_cstr ()
{
    return  ConstString<c...>{};
}

使用tuple的无模板构造以及tuple现在具有constexpr operator ==的事实,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;
    }
}

下一件事-定义开关情况代码:

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
基于这个post,我设法以更简单的方式来处理ConstString。工作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...>;
};

通过改变BOOST_PP_REPEAT(20, ELEMENT_OR_NULL, #value)中的第一个参数(20),我们可以控制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);
}
6yt4nkrj

6yt4nkrj2#

写起来很容易

switcher(expr)->*
caser(case0)->*[&]{
}->*
caser(case1)->*[&]{
};

构建一个静态大小的case0caseN的哈希表,动态填充它,测试与==的冲突,通过expr进行查找,并运行相应的lambda。
甚至可以支持caser(case3)->*caser(case4)->*lambda->*fallthrough
我看不出有什么迫切的需要。
我也不认为用C++17写这个有什么好处。

lbsnaicq

lbsnaicq3#

从C++11开始,你可以使用smilingthax/cttrie(cf. C/C++: switch for non-integers - esp. 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遍历它。代码块被 Package 在lambda表达式中,并在相应的叶子上执行。

jchrr9hc

jchrr9hc4#

这里是一个简单的解决方案,用于在C/C++中模拟开关情况。

**更新:**包括 * 继续 * 版本。早期版本不能在循环中使用 continue 语句。当在循环中使用时,常规的switch-case块可以执行 continue,正如预期的那样。但是因为我们在SWITCH-CASE宏中使用了 for 循环,所以 continue 只会带出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

示例:带有 continue的开关箱

EXECUTE
如果SWITCH块在循环中使用,并且我们碰巧在SWITCH中使用continue,那么我们需要用CONTINUE(而不是END)结束SWITCH

#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)
    }
}

输出:

in def (continuing)
in ghi (not breaking)
in DEFAULT
  • 需要在循环中使用SWITCH..CASE.. CONTINUE(如果在switch中需要continue,也是如此)
  • 默认情况下需要使用SWITCH..CASE.. END
  • 可以使用反向字符串比较。喜欢

SWITCH(“abc”)CASE(str1)END
这种比较可以打开大量的比较选项,并避免笨拙的if-else链。如果不逐个字符比较,则无法进行字符串比较,因此无法避免if-else链。至少SWITCH-CASE的代码看起来很可爱。但瓶颈在于

  • 3个额外变量
  • 5额外的任务和
  • 每个CASE进行1次额外(bool)比较

因此,开发人员在if-else和SWITCH-CASE之间选择的自由裁量权

fbcarpbf

fbcarpbf5#

对@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。

ukdjmx9f

ukdjmx9f6#

使用switch语句的最初原因是编译器可以将其Map到类似的机器操作。对于具有大量案例的开关,这产生非常有效的机器代码。
对于字符串,由于需要比较,这是不可能的,因此实现的效率要低得多;与if/else/else-if子句没有任何不同。C和 C++ 家族的目标仍然是允许在没有任何开销的情况下生成非常高效的机器代码,所以字符串上的开关不是一个有用的扩展-如果你真的需要更高效的话,有更有效的方法来编码。这还意味着在语言语法中添加一个“strcmp”,以及它的所有变化和变幻莫测--这不是一个好主意。
我怀疑这在任何时候对任何版本的C++来说都是一个很好的扩展。

tyu7yeag

tyu7yeag7#

这里有另一个解决方案。但这个版本也使用了一系列的比较。

  • 3个赋值(包括所有CASE字符串指针的Array)
  • string比较直到找到匹配
  • 递增整数直到找到匹配项

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

输出:

def
2q5ifsrm

2q5ifsrm8#

在C++17中,我利用std::find作为<algorithm>组的一部分。其思想是将所有case值字符串一起保存在一个可搜索的容器中(例如std::vector)。我们将尝试定位正在搜索的字符串,然后根据找到的int索引进行切换。
因此,让我们开始创建一个finder模板,例如:

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

如果在haystack中没有找到指针,find_case将返回**-1**,如果找到指针,则返回非负索引。
以下是使用示例:

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

作为模板的一个优点是我们还可以使用其他类型,例如std::wstring
现在,让我们看看索引上的开关:

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

不要忘记包含<vector><algorithm>头文件。
在这个答案的范围之外,还有更强大的实现,使用std搜索器和std::any,让我们在一个公共的switch语句下同时拥有字符串和整数类型的情况。

8ehkhllq

8ehkhllq9#

在聚会上,这里有一个我不久前提出的解决方案,它完全遵守所要求的语法,也适用于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();
}

需要注意的唯一区别是使用uswitch代替switch,使用ucase代替case,并在值周围添加括号(需要,因为这是一个宏)。
代码如下:https://github.com/falemagn/uberswitch

相关问题