【问题标题】:How to branch/switch on multiple conditions?如何在多个条件下分支/切换?
【发布时间】:2015-12-24 00:21:09
【问题描述】:

有没有一种方法可以在多个条件下分支而不编写看起来像一团糟的代码? C++11 或 C++14 中的语法糖将不胜感激。

#include <iostream>

enum state
{
    STATE_1,
    STATE_2,
    STATE_3,
    STATE_4,
    STATE_5,
    STATE_6,
    STATE_7,
    STATE_8,
};

state f(int a, bool b, const std::string& str)
{
    // How not to:
    if (a < 0)
    {
        if (b == false)
        {
            if (str != "morning")
            {
                return STATE_1;
            }
            else
            {
                return STATE_2;
            }
        }
        else
        {
            if (str != "morning")
            {
                return STATE_3;
            }
            else
            {
                return STATE_4;
            }
        }
    }
    else // a >= 0
    {
        if (b == false)
        {
            if (str != "morning")
            {
                return STATE_5;
            }
            else
            {
                return STATE_6;
            }
        }
        else
        {
            if (str != "morning")
            {
                return STATE_7;
            }
            else
            {
                return STATE_8;
            }
        }
    }
}

int main()
{
    std::cout << "State: " << f(1, true, "morning") << std::endl;
}

【问题讨论】:

    标签: c++ c++11


    【解决方案1】:

    可以在编译时将布尔(条件结果)列表嵌入到 POD 中,并在其上嵌入 switch

    用法:main.cpp

    #include <iostream> /* std::cout */
    #include "mswitch.h" /* mswitch, mcase */
    
    enum state
    {
        STATE_1,
        STATE_2,
        STATE_3,
        STATE_4,
        STATE_5,
        STATE_6,
        STATE_7,
        STATE_8,
    };
    
    state f(int a, bool b, const std::string& str)
    {
        mswitch(a >= 0, b == true, str == "morning")
        {
            mcase(false, false, false): return STATE_1;
            mcase(false, false, true) : return STATE_2;
            mcase(false, true, false) : return STATE_3;
            mcase(false, true, true)  : return STATE_4;
            mcase(true, false, false) : return STATE_5;
            mcase(true, false, true)  : return STATE_6;
            mcase(true, true, false)  : return STATE_7;
            mcase(true, true, true)   : return STATE_8;
        }
        return STATE_1;
    }
    
    int main()
    {
        std::cout << "State: " << f(1, true, "morning") << std::endl;
    }
    

    语法糖:mswitch.h

    #ifndef MSWITCH_GUARD_H
    #define MSWITCH_GUARD_H
    
    #include <initializer_list>
    #include <cstddef>
    
    namespace mswitch
    {
        constexpr long long encode(long long value, size_t size) { return value << 6 | (0x3F & size); }
    
        class mswitch
        {
            std::initializer_list<bool> _flags;
        public:
            mswitch(std::initializer_list<bool> const& l) : _flags(l) {}
            operator long long() const
            {
                long long result = 0;
                size_t index = 0;
                for (bool b : _flags) {
                    result |= b << index++;
                }
                return encode(result, _flags.size());
            }
        };
    
        template<bool head, bool... tail>
        struct mcase
        {
            constexpr mcase() = default;
            constexpr operator long long() const
            {
                return encode(tll(), 1+sizeof...(tail));
            }
            constexpr long long tll() const { return head | mcase<tail...>().tll() << 1; }
        };
    
        template<bool b>
        struct mcase<b>
        {
            constexpr mcase() = default;
            constexpr operator long long() const { return encode(tll(), 1); }
            constexpr long long tll() const { return b; }
        };
    }
    
    #define mswitch(head, ...) switch(mswitch::mswitch{head, __VA_ARGS__})
    #define mcase(head, ...) case mswitch::mcase<head, __VA_ARGS__>()
    
    #endif // MSWITCH_GUARD_H
    

    编译为 g++ -std=c++14 -O2 -Wall -pedantic main.cpp

    工作原理

    mswitchmcase 对象只是在布尔列表和switchable long long 之间构建(如果可能,在编译时使用constexpr 函数)。由于mcases 被赋予了编译时常量,所有switch 标签实际上本身就是连续的编译时常量。

    【讨论】:

    • @Bathsheba 在我给出的用例中,mswitch::mswitch 对象是使用运行时值构造的。在这种情况下,从布尔列表到long long 的转换是在运行时完成的。
    • 谢谢。所以从各个方面来说,这都是一个不错的答案。
    【解决方案2】:

    我会为此做一个查找表:

    #include <iostream>
    #include <string>
    
    enum state {
      STATE_1,
      STATE_2,
      STATE_3,
      STATE_4,
      STATE_5,
      STATE_6,
      STATE_7,
      STATE_8,
    };
    
    state f(int a, bool b, const std::string& str) {
      static const state table[2][2][2] = {
        STATE_8, // 0, 0, 0
        STATE_7, // 0, 0, 1
        STATE_6, // 0, 1, 0
        STATE_5, // 0, 1, 1
        STATE_4, // 1, 0, 0
        STATE_3, // 1, 0, 1
        STATE_2, // 1, 1, 0
        STATE_1  // 1, 1, 1
      };
      return table[a < 0][b == false][str != "morning"];
    }
    
    int main() {
      std::cout << f(1, true, "morning") << std::endl;
    }
    

    【讨论】:

    • 是否指定true 转换为1?我确定false 转换为0,但如果我没记错的话,true 转换为0 以外的任何东西。无论如何,好方法。
    • @YSC 指定的很好。根据积分提升,bool 类型可以转换为 int,值 false 变为 ​0​ 和 true 变为 1。请参阅此cppref 页面。
    • 与大量模板的答案不同,这实际上确实看起来更好,并且可以在其上下文中立即理解。
    • 我喜欢这样,但是表的初始化定义好了吗?初始化列表是平的,这不会引发警告吗?
    • @StoryTeller 它的行为是明确定义的,尽管编译器通常会生成警告并建议不要省略嵌套大括号以提高可读性。虽然在大多数情况下,完整的嵌套大括号确实可以提高可读性并使代码更清晰,但在这种特殊情况下,它们并没有。
    【解决方案3】:

    我同意,模式匹配非常适合那里。不幸的是,内置的switch 在 C++ 中非常有限。

    编译时布尔包的实现非常简单。

    #include <type_traits>
    
    namespace detail
    {
        constexpr std::size_t pack_bool(std::size_t result)
        {
            return result;
        }
    
        template<typename T, typename... Ts>
        constexpr std::size_t pack_bool(std::size_t result, T arg, Ts... args)
        {
            static_assert(std::is_same<bool, T>::value, "boolean expected");
            return pack_bool((result << 1) | arg, args...);
        }
    }
    
    template<typename T, typename... Ts>
    constexpr std::size_t pack_bool(T arg, Ts... args)
    {
        static_assert(std::is_same<bool, T>::value, "boolean expected");
        return detail::pack_bool(arg, args...);
    }
    

    现在,您可以在switch 语句中使用它

    #include <iostream>
    
    enum state
    {
        STATE_1,
        STATE_2,
        STATE_3,
        STATE_4,
        STATE_5,
        STATE_6,
        STATE_7,
        STATE_8,
    };
    
    state f(int a, bool b, const std::string& str)
    {
        switch (pack_bool(a >= 0, b == true, str == "morning"))
        {
            case pack_bool(false, false, false) : return STATE_1;
            case pack_bool(false, false, true)  : return STATE_2;
            case pack_bool(false, true,  false) : return STATE_3;
            case pack_bool(false, true,  true)  : return STATE_4;
            case pack_bool(true,  false, false) : return STATE_5;
            case pack_bool(true,  false, true)  : return STATE_6;
            case pack_bool(true,  true,  false) : return STATE_7;
            case pack_bool(true,  true,  true)  : return STATE_8;
        }
        return STATE_1;
    }
    
    int main()
    {
        std::cout << "State: " << f(1, true, "morning") << std::endl;
    }
    

    【讨论】:

      【解决方案4】:

      这是我的版本:

      特点:

      1. 保留编译器检查遗漏的情况,并提供有关遗漏的情况的信息性消息。

      2. 案例的编译时评估意味着零运行时开销

      3. 没有宏会污染全局命名空间并随机阻止仅标头库工作:-)

      缺点:

      1. 需要预定义一些样板枚举(仅一次,在我为您完成的库中)

      代码:

      #include <iostream>
      #include <utility>
      #include <sstream>
      #include <string>
      
      namespace detail{
      
          template<size_t N> struct boolean_value;
          template<size_t N> using boolean_value_t = typename boolean_value<N>::type;
          template<size_t N> constexpr auto to_int(boolean_value_t<N> b) { return static_cast<int>(b); };
          template<size_t N> constexpr auto to_boolean_value(int i) { return static_cast<boolean_value_t<N>>(i); };
      
          template<> struct boolean_value<1> {
              enum type { bit0, bit1 };
          };
      
          template<> struct boolean_value<2> {
              enum type { bit00, bit01, bit10, bit11 };
          };
      
          template<> struct boolean_value<3> {
              enum type { bit000, bit001, bit010, bit011, bit100, bit101, bit110, bit111 };
          };
      
          template<class...Args, size_t...Is>
          static constexpr auto make_bitfield(std::tuple<Args...> t, std::index_sequence<Is...>)
          {
      #if __cplusplus > 201402L
              int accum = (0 | ... | (std::get<Is>(t) ? (1 << Is) : 0));
      #else
              int accum = 0;
              using expand = int[];
              (void) expand { (std::get<Is>(t) ? accum |= (1 << Is) : 0) ... };
      #endif
              return to_boolean_value<sizeof...(Is)>(accum);
          }
      
      }
      
      template<class...Args>
      constexpr
      auto mcase(Args&&...args)
      {
          return detail::make_bitfield(std::make_tuple(bool(std::forward<Args>(args))...),
                                       std::index_sequence_for<Args...>());
      }
      
      // little function to defeat the optimiser, otherwise clang inlines the whole program!
      auto get_result()
      {
          using namespace std;
      
          istringstream ss("foo 2");
          auto result = tuple<string, int>();
          ss >> get<0>(result) >> get<1>(result);
          return result;
      }
      
      int main()
      {
          using namespace std;
          const auto result = get_result();
          const auto& s1 = std::get<0>(result);
          const auto& v1 = std::get<1>(result);
      
          switch(mcase(s1 == "foo"s, v1 == 2))
          {
              case mcase(true, true):
                  cout << mcase(true, true) << endl;
                  break;
      
              case mcase(false, false):
                  cout << mcase(false, false) << endl;
                  break;
          }
          return 0;
      }
      

      示例编译器输出:

      ./mswitch.cpp:114:12: warning: enumeration values 'bit01' and 'bit10' not handled in switch [-Wswitch]
          switch(mcase(s1 == "foo"s, v1 == 2))
                 ^
      1 warning generated.
      ./mswitch.cpp:114:12: warning: enumeration values 'bit01' and 'bit10' not handled in switch [-Wswitch]
          switch(mcase(s1 == "foo"s, v1 == 2))
                 ^
      1 warning generated.
      

      运行时输出:

      3
      

      【讨论】:

      • 太棒了!这是 Lingxi 答案的一个非常好的替代方案,我们正在用简单性换取编译器检查和良好的编译警告。不过,我只能接受两个答案之一。
      • @YSC 如果凌希没有贴出他的答案,我不会想到我的,所以应该归功于他。
      【解决方案5】:

      作为使用 C++ 基本功能的替代答案,您还可以考虑使用三元运算符。

      enum state
      {
          STATE_1,
          STATE_2,
          STATE_3,
          STATE_4,
          STATE_5,
          STATE_6,
          STATE_7,
          STATE_8,
      };
      
      state f(int a, bool b, const std::string& str)
      {
          // How not to:
          if (a < 0)
              return b == true ? (str == "morning" ? STATE_4 : STATE_3) : (str == "morning" ? STATE_2 : STATE_1);
          else // a >= 0
              return b == true ? (str == "morning" ? STATE_8 : STATE_7) : (str == "morning" ? STATE_6 : STATE_5);
      }
      
      int main()
      {
          std::cout << "State: " << f(1, true, "morning") << std::endl;
      }
      

      仅使用 basic 运算符时,结果非常紧凑。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-08-23
        • 2021-09-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-07-22
        相关资源
        最近更新 更多