【问题标题】:Compact way to write if(..) statement with many equalities编写具有多个等式的 if(..) 语句的紧凑方法
【发布时间】:2016-06-29 07:03:27
【问题描述】:

有没有更好的方法来编写这样的代码:

if (var == "first case" or var == "second case" or var == "third case" or ...)

在 Python 中我可以写:

if var in ("first case", "second case", "third case", ...)

这也让我有机会轻松通过好选项列表:

good_values = "first case", "second case", "third case"
if var in good_values

这只是一个例子:var 的类型可能与字符串不同,但我只对替代 (or) 比较 (==) 感兴趣。 var 可能不是const,而选项列表在编译时是已知的。

专业奖金:

  • or的懒惰
  • 编译时循环展开
  • 易于扩展到==以外的其他运算符

【问题讨论】:

  • 好吧, std::any_of,但它不适用于这样的“文字”列表。因此,没有从 Python 表达式到类似 C++ 表达式的直接转换。使用std::any_ofstd::initializer_list 制作模板化的in 函数应该不难。
  • 你可以写一个像template<typename T>bool eq(T what, T a, T b, T c){ return what == a || what == b || what == c; }这样的模板函数并使用它:if(eq(var, "first", "second", "third"))
  • @Ruggero Turra 而不是像 any_of 这样的新关键字,你可以更喜欢for() 循环,这将使程序易于阅读。
  • 压缩代码是不清晰的代码。 “紧凑”不是编码的优点。
  • @Almo:我完全不同意。当然,有些东西非常简洁(我在看你,正则表达式)以至于普通人认为它们不清晰,但是,在所有其他条件相同的情况下,更少的代码总是比更多的更好。

标签: c++ if-statement


【解决方案1】:

首先,我推荐使用for 循环,它既最简单又 最易读的解决方案:

for (i = 0; i < n; i++) {
   if (var == eq[i]) {
      // if true
      break;
   }
}

但是,也可以使用其他一些方法,例如,std::all_ofstd::any_ofstd::none_of(在#include &lt;algorithm&gt;)。

让我们看一下包含上述所有关键字的简单示例程序

#include <vector>
#include <numeric>
#include <algorithm>
#include <iterator>
#include <iostream>
#include <functional>

int main()
{
    std::vector<int> v(10, 2);
    std::partial_sum(v.cbegin(), v.cend(), v.begin());
    std::cout << "Among the numbers: ";
    std::copy(v.cbegin(), v.cend(), std::ostream_iterator<int>(std::cout, " "));
    std::cout << '\\n';

    if (std::all_of(v.cbegin(), v.cend(), [](int i){ return i % 2 == 0; })) 
    {
        std::cout << "All numbers are even\\n";
    }
    if (std::none_of(v.cbegin(), v.cend(), std::bind(std::modulus<int>(),
                                  std::placeholders::_1, 2))) 
    {
        std::cout << "None of them are odd\\n";
    }
    struct DivisibleBy
    {
        const int d;
        DivisibleBy(int n) : d(n) {}
        bool operator()(int n) const { return n % d == 0; }
    };

    if (std::any_of(v.cbegin(), v.cend(), DivisibleBy(7))) 
    {
        std::cout << "At least one number is divisible by 7\\n";
    }
}

【讨论】:

  • 我猜你应该在第二个解决方案中添加一个break
  • @RuggeroTurra 是的,它会节省时间
  • for 循环是最快和最易读的版本,所以应该没有理由使用其他任何东西。
  • 是的@Lundin 我也更喜欢 for 循环版本,它真的很容易编程
  • 当你推荐使用好的解决方案时,为什么你会推荐使用错误的(旧式 for 循环)?
【解决方案2】:

any_of 算法在这里可以很好地工作:

#include <algorithm>
#include <initializer_list>

auto tokens = { "abc", "def", "ghi" };

bool b = std::any_of(tokens.begin(), tokens.end(),
                     [&var](const char * s) { return s == var; });

(您可能希望将tokens 的范围限制为所需的最小上下文。)

或者你创建一个包装模板:

#include <algorithm>
#include <initializer_list>
#include <utility>

template <typename T, typename F>
bool any_of_c(const std::initializer_list<T> & il, F && f)
{
    return std::any_of(il.begin(), il.end(), std::forward<F>(f));
}

用法:

bool b = any_of_c({"abc", "def", "ghi"},
                  [&var](const char * s) { return s == var; });

【讨论】:

  • 小心!您的声明s == val 正在比较指针!您可能想使用std::strcmp 或其亲属之一。
  • @JoshuaGreen: val 当然是std::string_view :-) (想想看,初始化列表应该是字符串视图的列表,这样可以一劳永逸地解决问题全部。)无论如何,这就是OP所要求的。我假设他知道他比较的语义。
  • @Lundin:原始代码是读者的负担。它迫使读者阅读你的循环体。为概念命名并进行命名调用允许读者跳过“知道你的意思”这一行,并相信经过测试的库实现正确地实现了意图。从根本上说,每个系统都受到其不断增长的复杂性的限制;命名抽象(算法)是降低复杂性的主要策略之一。
  • @Lundin:这与能力无关。这是关于打破复杂性。读者有比看你的循环更好的事情要做。
  • @Lundin 与其说是“理解for 循环有多难”,不如说是“发现细微错误有多容易_”。例如。开始/结束时出现偏离 1 的错误(如果您有基于 1 而不是更常见的基于 0 的数组,则很容易);在更复杂的循环体中,可能会出现副作用或错误处理方面的错误。尽管 for 循环是微不足道的,但通用迭代背后的 想法 是消除某些常见错误。在琐碎的例子中,获得的收益是微乎其微的,您不希望因引入其他错误而失去它们。但是 foreach、ifany、ifall、ifnone 是简单而强大的概念。
【解决方案3】:

如果你想扩展它的编译时间,你可以使用这样的东西

template<class T1, class T2>
bool isin(T1&& t1, T2&& t2) {
   return t1 == t2;
}

template<class T1, class T2, class... Ts>
bool isin(T1&& t1 , T2&& t2, T2&&... ts) {
   return t1 == t2 || isin(t1, ts...);
}

std::string my_var = ...; // somewhere in the code
...
bool b = isin(my_var, "fun", "gun", "hun");

我实际上并没有对其进行测试,这个想法来自 Alexandrescu 的“Variadic templates are funadic”演讲。所以对于细节(和正确的实现)请注意。

编辑: 在 c++17 中,他们引入了一个不错的 fold expression 语法

template<typename... Args>
bool all(Args... args) { return (... && args); }

bool b = all(true, true, true, false);
 // within all(), the unary left fold expands as
 //  return ((true && true) && true) && false;
 // b is false

【讨论】:

  • 这需要一一列出选项。有没有办法写类似isin(my_var, {"fun", "gun", "hun"})isin(my_vary, options_vector)
  • 第一个选项应该是可能的,但是扩展 initializer_list 编译时间......真的不值得。对于第二个选项,我宁愿使用 Kerrek 答案,或者将其包装成一个更方便的界面,就像你要求的那样。因为这听起来更像是运行时的东西。
  • 选项列表在编译时是已知的
  • @RuggeroTurra:(未经测试的)代码实际上回答了您的问题,用于检查容器中是否有条目,有 std::find() 或更专业的函数(binary_search、lower_bound、count、.. .)
  • @stefaanv 我看到了 Ruggero 要求的好的用例。但是解压initializer_list编译时间可不是小事,我稍后再试试……希望能更新。
【解决方案4】:

您可以使用 std::set 来测试 var 是否属于它。 (启用 c++11 编译)

#include <iostream>
#include <set>

int main()
{
    std::string el = "abc";

    if (std::set<std::string>({"abc", "def", "ghi"}).count(el))
        std::cout << "abc belongs to {\"abc\", \"def\", \"ghi\"}" << std::endl;

    return 0;
}

优点是std::set&lt;std::string&gt;::count 工作在O(log(n)) 时间(其中n 是要测试的字符串数)与非紧凑型if 相比,通常是O(n)。缺点是集合的构造需要O(n*log(n))。所以,构造一次,比如:

static std::set<std::string> the_set = {"abc", "def", "ghi"};

但是,IMO 最好让条件保持原样,除非它包含超过 10 个要检查的字符串。使用 std::set 进行此类测试的性能优势仅出现在 big n 中。此外,简单的非紧凑 if 对于普通 c++ 开发人员来说更容易阅读。

【讨论】:

    【解决方案5】:

    如果您可以访问 C++14(不确定这是否适用于 C++11),您可以编写如下内容:

    template <typename T, typename L = std::initializer_list<T>>
    constexpr bool is_one_of(const T& value, const L& list)
    {
        return std::any_of(std::begin(list), std::end(list), [&value](const T& element) { return element == value; });
    };
    

    调用如下所示:

    std::string test_case = ...;
    if (is_one_of<std::string>(test_case, { "first case", "second case", "third case" })) {...}
    

    或者像这样

    std::string test_case = ...;
    std::vector<std::string> allowedCases{ "first case", "second case", "third case" };
    if (is_one_of<std::string>(test_case, allowedCases)) {...}
    

    如果您不喜欢将允许的案例“包装”成列表类型,您还可以编写一个小辅助函数,如下所示:

    template <typename T, typename...L>
    constexpr bool is_one_of(const T& value, const T& first, const L&... next) //First is used to be distinct
    {
        return is_one_of(value, std::initializer_list<T>{first, next...});
    };
    

    这将允许您这样称呼它:

    std::string test_case = ...;
    if (is_one_of<std::string>(test_case, "first case", "second case", "third case" )) {...}
    

    Complete example on Coliru

    【讨论】:

      【解决方案6】:

      最接近的应该是:

      template <class K, class U, class = decltype(std::declval<K>() == std::declval<U>())>
      bool in(K&& key, std::initializer_list<U> vals)
      {
          return std::find(vals.begin(), vals.end(), key) != vals.end();
      }
      

      我们需要采用initializer_list&lt;U&gt; 类型的参数,以便我们可以传入一个braced-init-list,如{a,b,c}。这会复制元素,但我们可能会这样做,因为我们提供的是文字,所以可能没什么大不了的。

      我们可以这样使用:

      std::string var = "hi";    
      bool b = in(var, {"abc", "def", "ghi", "hi"});
      std::cout << b << std::endl; // true
      

      【讨论】:

      • 是否可以使用循环展开来代替std::find
      【解决方案7】:

      值得注意的是,在我见过的大多数 Java 和 C++ 代码中,列出 3 个左右的条件是公认的做法。它肯定比“聪明”的解决方案更具可读性。如果这种情况经常发生,那就是一个很大的拖累,无论如何这都是一种设计味道,模板化或多态方法可能有助于避免这种情况。

      所以我的答案是“空”操作。继续做更冗长的事情,这是最容易接受的。

      【讨论】:

      • 我同意,但重构是我无法控制的。我只需要一个语法糖。
      • 根据我的经验,做最冗长的事情很快就会变得如此冗长,以至于无法弄清楚实际测试的是什么。只需测试 4 个变量和乐高命名,测试将轻松在屏幕上填充 120 多个字符。一个接一个地添加 16 或 20 个这样的测试,你没有机会弄清楚发生了什么。
      • @Clearer 单元测试通常应该没有逻辑。出于这个原因,它们往往非常冗长但非常易读且无错误,因为它们是相互独立的测试。我不确定您是否认为 24​​00 个字符太多而无法辨认?我的测试文件往往比我的源文件大 1-2 倍,而源文件往往运行在 100 或 1000 行左右。如果您遇到了大约 10 条推文的字符不可读的重大问题,那么我不确定您如何更好地编写更短的代码。
      • @djechlin 我不明白单元测试是如何因为没有逻辑而变得可读的(我不同意它们通常应该如此)。如果您的测试失败,我想知道哪个条件失败,而不是哪一组条件失败。
      • @Clearer 每个条件一个测试。
      【解决方案8】:

      好吧,你想要激进的语言修改。具体来说,您想创建自己的运算符。准备好了吗?

      语法

      我将修改语法以使用 C 和 C++ 样式的列表:

      if (x in {x0, ...}) ...
      

      此外,我们将让新的 in 运算符应用于定义了begin()end() 的任何容器:

      if (x in my_vector) ...
      

      有一个警告:它不是一个真正的运算符,因此它必须始终像它自己的表达式一样用括号括起来:

      bool ok = (x in my_array);
      
      my_function( (x in some_sequence) );
      

      代码

      首先要注意的是,RLM 通常需要一些宏和操作符滥用。幸运的是,对于一个简单的成员谓词来说,滥用实际上并没有那么糟糕。

      #ifndef DUTHOMHAS_IN_OPERATOR_HPP
      #define DUTHOMHAS_IN_OPERATOR_HPP
      
      #include <algorithm>
      #include <initializer_list>
      #include <iterator>
      #include <type_traits>
      #include <vector>
      
      //----------------------------------------------------------------------------
      // The 'in' operator is magically defined to operate on any container you give it
      #define in , in_container() =
      
      //----------------------------------------------------------------------------
      // The reverse-argument membership predicate is defined as the lowest-precedence 
      // operator available. And conveniently, it will not likely collide with anything.
      template <typename T, typename Container>
      typename std::enable_if <!std::is_same <Container, T> ::value, bool> ::type
      operator , ( const T& x, const Container& xs )
      {
        using std::begin;
        using std::end;
        return std::find( begin(xs), end(xs), x ) != end(xs);
      }
      
      template <typename T, typename Container>
      typename std::enable_if <std::is_same <Container, T> ::value, bool> ::type
      operator , ( const T& x, const Container& y )
      {
        return x == y;
      }
      
      //----------------------------------------------------------------------------
      // This thunk is used to accept any type of container without need for 
      // special syntax when used.
      struct in_container
      {
        template <typename Container>
        const Container& operator = ( const Container& container )
        {
          return container;
        }
      
        template <typename T>
        std::vector <T> operator = ( std::initializer_list <T> xs )
        {
          return std::vector <T> ( xs );
        }
      };
      
      #endif
      

      用法

      太棒了!现在我们可以在 all 中使用它,就像您期望 in 运算符有用的方式一样。根据您的特殊兴趣,请参见示例 3:

      #include <iostream>
      #include <set>
      #include <string>
      using namespace std;
      
      void f( const string& s, const vector <string> & ss ) { cout << "nope\n\n"; }
      void f( bool b ) { cout << "fooey!\n\n"; }
      
      int main()
      {
        cout << 
          "I understand three primes by digit or by name.\n"
          "Type \"q\" to \"quit\".\n\n";
      
        while (true)
        {
          string s;
          cout << "s? ";
          getline( cin, s );
      
          // Example 1: arrays 
          const char* quits[] = { "quit", "q" };
          if (s in quits) 
            break;
      
          // Example 2: vectors
          vector <string> digits { "2", "3", "5" };
          if (s in digits)
          {
            cout << "a prime digit\n\n";
            continue;
          }
      
          // Example 3: literals
          if (s in {"two", "three", "five"})
          {
            cout << "a prime name!\n\n";
            continue;
          }
      
          // Example 4: sets
          set <const char*> favorites{ "7", "seven" };
          if (s in favorites)
          {
            cout << "a favorite prime!\n\n";
            continue;
          }
      
          // Example 5: sets, part deux
          if (s in set <string> { "TWO", "THREE", "FIVE", "SEVEN" })
          {
            cout << "(ouch! don't shout!)\n\n";
            continue;
          }
      
          // Example 6: operator weirdness
          if (s[0] in string("014") + "689")
          {
            cout << "not prime\n\n";
            continue;
          }
      
          // Example 7: argument lists unaffected    
          f( s, digits );
        }
        cout << "bye\n";
      }
      

      可能的改进

      总有一些事情可以针对您的特定目的改进代码。您可以添加一个 ni (not-in)运算符(添加一个新的 thunk 容器类型)。您可以将 thunk 容器包装在命名空间中(一个好主意)。您可以专注于 std::set 之类的东西,以使用 .count() 成员函数而不是 O(n) 搜索。等等。

      您的其他顾虑

      • const vs mutable:不是问题;两者都可以与操作员一起使用
      • or 的懒惰:从技术上讲,or 不是懒惰,它是短路的。 std::find() 算法也以同样的方式短路。
      • 编译时循环展开:此处并不适用。您的原始代码没有使用循环;虽然 std::find() 确实如此,但可能发生的任何循环展开都取决于编译器。
      • 易于扩展到== 以外的运算符:这实际上是一个单独的问题;您不再关注简单的成员资格谓词,而是正在考虑使用功能性折叠过滤器。完全有可能创建一个算法来做到这一点,但标准库提供了 any_of() 函数,它正是这样做的。 (它只是不如我们的 RLM 'in' 运算符漂亮。也就是说,任何 C++ 程序员都会很容易理解它。这里已经提供了这样的答案。)

      希望这会有所帮助。

      【讨论】:

      • 这是对 C++ 的很好的滥用。我喜欢它。
      【解决方案9】:

      您可以使用开关盒。而不是一个单独的案例列表,您可以:

      包括

      使用命名空间标准;

      int main() { 字符等级 = 'B';

      switch(grade)
      {
      case 'A' :
      case 'B' :
      case 'C' :
          cout << "Well done" << endl;
          break;
      case 'D' :
          cout << "You passed" << endl;
          break;
      case 'F' :
          cout << "Better try again" << endl;
          break;
      
      default :
          cout << "Invalid grade" << endl;
      
      }
      
      cout << "Your grade is " << grade << endl;
      
      return 0;
      

      }

      因此您可以将结果组合在一起:A、B 和 C 将输出“做得好”。 我从教程点拿了这个例子: http://www.tutorialspoint.com/cplusplus/cpp_switch_statement.htm

      【讨论】:

      • 绝对不比ifs的列表好
      • 显然这是有争议的:[链接] (stackoverflow.com/questions/97987/…) 在这里:lnk
      • 我要求更紧凑版本,而不是更快
      • 嗯..你问>“有没有更好的方法来编写这样的代码:”?我想这是一个相当开放的问题。如果我误解了你,我深表歉意:-)
      猜你喜欢
      • 1970-01-01
      • 2012-12-13
      • 1970-01-01
      • 2012-06-22
      • 2011-12-15
      • 1970-01-01
      • 1970-01-01
      • 2022-11-01
      • 2021-12-15
      相关资源
      最近更新 更多