Compact way to write if(..) statement with many equalities

C++If Statement

C++ Problem Overview


Is there a better way to write code like this:

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

In Python I can write:

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

which also gives me the opportunity to easily pass the list of good options:

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

This is just an example: the type of var may be different from a string, but I am only interested in alternative (or) comparisons (==). var may be non-const, while the list of options is known at compile time.

Pro bonus:

  • laziness of or
  • compile time loop unrolling
  • easy to extend to other operators than ==

C++ Solutions


Solution 1 - C++

if you want to expand it compile time you can use something like this

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");

I did not test it actually, and the idea comes from Alexandrescu's 'Variadic templates are funadic' talk. So for the details (and proper implementation) watch that.

Edit: in c++17 they introduced a nice fold expression syntax

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

Solution 2 - C++

The any_of algorithm could work reasonably well here:

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

(You may wish to constrain the scope of tokens to the minimal required context.)

Or you create a wrapper template:

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

Usage:

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

Solution 3 - C++

Alrighty then, you want Radical Language Modification. Specifically, you want to create your own operator. Ready?

Syntax

I'm going to amend the syntax to use a C and C++-styled list:

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

Additionally, we'll let our new in operator apply to any container for which begin() and end() are defined:

if (x in my_vector) ...

There is one caveat: it is not a true operator and so it must always be parenthesized as it's own expression:

bool ok = (x in my_array);

my_function( (x in some_sequence) );

The code

The first thing to be aware is that RLM often requires some macro and operator abuse. Fortunately, for a simple membership predicate, the abuse is actually not that bad.

#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

Usage

Great! Now we can use it in all the ways you would expect an in operator to be useful. According to your particular interest, see example 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";
}

Potential improvements

There are always things that can be done to improve the code for your specific purposes. You can add a ni (not-in) operator (Add a new thunk container type). You can wrap the thunk containers in a namespace (a good idea). You can specialize on things like std::set to use the .count() member function instead of the O(n) search. Etc.

Your other concerns

  • const vs mutable : not an issue; both are usable with the operator
  • laziness of or : Technically, or is not lazy, it is short-circuited. The std::find() algorithm also short-circuits in the same way.
  • compile time loop unrolling : not really applicable here. Your original code did not use loops; while std::find() does, any loop unrolling that may occur is up to the compiler.
  • easy to extend to operators other than == : That actually is a separate issue; you are no longer looking at a simple membership predicate, but are now considering a functional fold-filter. It is entirely possible to create an algorithm that does that, but the Standard Library provides the any_of() function, which does exactly that. (It's just not as pretty as our RLM 'in' operator. That said, any C++ programmer will understand it easily. Such answers have already been proffered here.)

Hope this helps.

Solution 4 - C++

> First, I recommend using a for loop, which is both the easiest and > most readable solution: >
> for (i = 0; i < n; i++) { > if (var == eq[i]) { > // if true > break; > } > }

However, some other methods also available, e.g., std::all_of, std::any_of, std::none_of (in #include <algorithm>).

Let us look at the simple example program which contains all the above keywords

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

Solution 5 - C++

You may use std::set to test if var belongs to it. (Compile with c++11 enabled)

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

The advantage is that std::set<std::string>::count works in O(log(n)) time (where is n is number of strings to test) comparing to non compact if witch is O(n) in general. The disadvantage is that construction of the set takes O(n*log(n)). So, construct it once, like:

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

But, IMO it would be better to leave the condition as is, unless it contains more than 10 strings to check. The performance advantages of using std::set for such a test appears only for big n. Also, simple non compact if is easier to read for average c++ developer.

Solution 6 - C++

The closest thing would be something like:

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

We need to take an argument of type initializer_list<U> so that we can pass in a braced-init-list like {a,b,c}. This copies the elements, but presumably we're going doing this because we're providing literals so probably not a big deal.

We can use that like so:

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

Solution 7 - C++

If you have access to C++14 (not sure if this works with C++11) you could write something like this:

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

A call would look like this:

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

or like this

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

If you don't like to "wrap" the allowed cases into a list type you can also write a little helper function like this:

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

This will allow you to call it like this:

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

Complete example on Coliru

Solution 8 - C++

Worth noting that in most Java and C++ code I've seen, listing 3 or so conditionals out is the accepted practice. It's certainly more readable than "clever" solutions. If this happens so often it's a major drag, that's a design smell anyway and a templated or polymorphic approach would probably help avoid this.

So my answer is the "null" operation. Just keep doing the more verbose thing, it's most accepted.

Solution 9 - C++

You could use a switch case. Instead of having a list of separate cases you could have :

#include

using namespace std;

int main () { char grade = '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;

}

So you can group your results together: A, B and C will output "well done". I took this example from Tutorials Point: http://www.tutorialspoint.com/cplusplus/cpp_switch_statement.htm

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRuggero TurraView Question on Stackoverflow
Solution 1 - C++SlavaView Answer on Stackoverflow
Solution 2 - C++Kerrek SBView Answer on Stackoverflow
Solution 3 - C++DúthomhasView Answer on Stackoverflow
Solution 4 - C++Embedded CView Answer on Stackoverflow
Solution 5 - C++ivaigultView Answer on Stackoverflow
Solution 6 - C++BarryView Answer on Stackoverflow
Solution 7 - C++Simon KraemerView Answer on Stackoverflow
Solution 8 - C++djechlinView Answer on Stackoverflow
Solution 9 - C++Mike CaveView Answer on Stackoverflow