Express one of multiple options in a nice way

We often find ourselves writing if statements where a variable is compared with several values either to check if it matches one of them or that it doesn’t match any. Here is an example:

int option = ...;

// at least a value is matched
if (option == 12 || option == 23 || option == 42)
{
   std::cout << "it's a good option\n";
}

// no value is matched
if (option != 12 && option != 23 && option != 42)
{
   std::cout << "it's a bad option\n";
}

This example has three comparison values for each case, but it could be 5, or 10, or any number. If it’s too many then perhaps a different approach should be taken. However, the question is, how do we express this in a simpler way in C++, rather than a long if condition?

Before we continue, please ignore the magic numbers used here. They are just some values to show the point in a simple manner. In real code, you should use enums, or constexpr values, or (evil option) macros. Here is a real example from some code I’m working with:

if (iCtrlType != CTRLTYPE_BUTTON &&
    iCtrlType != CTRLTYPE_CHECKBOX &&
    iCtrlType != CTRLTYPE_COMBOBOX &&
    iCtrlType != CTRLTYPE_COMBOGRID &&
    iCtrlType != CTRLTYPE_DROPDOWNCAL &&
    iCtrlType != CTRLTYPE_EDIT)
{
   // do something
}

In SQL, we can write statements of the following forms:

SELECT column-names
FROM table-name
WHERE column-name IN (values) 
SELECT column-names
FROM table-name
WHERE column-name NOT IN (values)

Can we achieve something similar to the SQL IN operator in C++? Well, in C++11 we have std::all_of, std::any_of, std::none_of that can help express the same intent. Using any_of and none_of we can rewrite the two if statements above as following:

std::vector<int> good_options{ 12, 23, 42 };
if (std::any_of(good_options.begin(), good_options.end(), [option](int const o) { return option == o; }))
{
   std::cout << "it's a good option\n";
}

if (std::none_of(good_options.begin(), good_options.end(), [option](int const o) { return option == o; }))
{
   std::cout << "it's a bad option\n";
}

This is very verbose, and personally, I don’t like it at all. I don’t like it because you need to put the values in a container (a vector here) and you need to supply iterators to the beginning and the end of the container, as well as a lambda to compare your value to every value in the container. There are simpler ways to do this.

In C++20, we have ranges, and the ranges library provides a new implementation for these algorithms, called std::ranges::all_of, std::ranges::any_of, std::ranges::none_of. Using these algorithms, we can simply supply a range (such as a container) instead of the pair of begin-end iterators. Therefore, our code would look as the following:

std::vector<int> good_options{ 12, 23, 42 };

if (std::ranges::any_of(good_options, [option](int const o) { return option == o; }))
{
   std::cout << "it's a good option\n";
}

if (std::ranges::none_of(good_options, [option](int const o) { return option == o; }))
{
   std::cout << "it's a bad option\n";
}

This is simpler, but I’m still not satisfied. We still need to put the values in a container. It’s not possible to simplify the code as follows:

if (std::ranges::any_of({ 12, 23, 42 }, [option](int const o) { return option == o; }))
{
   std::cout << "it's a good option\n";
}

That’s closer to the code I would like to be able to write. That code is shown below:

if (any_of({ 12, 23, 42 }, option))
{
   std::cout << "it's a good option\n";
}

if (none_of({ 12, 23, 42 }, option))
{
    std::cout << "it's a bad option\n";
}

This is an in-place sequence of values and a variable, instead of a lambda. This does not work out of the box with any standard algorithm, but we can easily write this any_of and none_of ourselves. A possible implementation is listed in the following snippet:

template <typename T>
bool any_of(std::initializer_list<T> r, T const v)
{
   return std::any_of(r.begin(), r.end(), [v](int const x) { return v == x; });
}

template <typename T>
bool none_of(std::initializer_list<T> r, T const v)
{
   return std::none_of(r.begin(), r.end(), [v](int const x) { return v == x; });
}

I know people will say that std::initializer_list is not a container and should not be used as such, but I don’t really feel it is used as one in this snippet. It basically holds a temporary sequence of values that are iterated with the std::any_of and std::none_of_ algorithms. This implementation will enable us to write code as in the previous snippet.

However, there is one more simplification that we could think of, which is shown here:

if (any_of(option, 12, 23, 42))
{
   std::cout << "it's a good option\n";
}

if (none_of(option, 12, 23, 42))
{
   std::cout << "it's a bad option\n";
}

There is no sequence of values, just a variable number of arguments, passed to a function. That means, this time, the implementation of any_of and none_of should be based on variadic templates. The way I wrote it, using fold expressions, is as follows:

template <typename T, typename ...Args>
bool any_of(T const v, Args&&... args)
{
   return ((args == v) || ...);
}

template <typename T, typename ...Args>
bool none_of(T const v, Args&&... args)
{
   return ((args != v) && ...);
}

This time, the variable is supplied as the first argument, and the values to test against follow it. Unfortunately, this implementation allows calls without any values to compare to, such as any_of(option). However, that is relatively simple to avoid by adding a static_assert statement, as follows:

template <typename T, typename ...Args>
bool any_of(T const v, Args&&... args)
{
   static_assert(sizeof...(args) > 0, "You need to supply at least one argument.");
   return ((args == v) || ...);
}

template <typename T, typename ...Args>
bool none_of(T const v, Args&&... args)
{
   static_assert(sizeof...(args) > 0, "You need to supply at least one argument.");
   return ((args != v) && ...);
}

If you don’t like static_asserts and you are using C++20, then you can use constraints to require the parameter pack to have at least one element. The change is relatively simple and looks as the following:

template <typename T, typename ...Args>
bool any_of(T const v, Args&&... args) requires (sizeof...(args) > 0)
{
   return ((args == v) || ...);
}

template <typename T, typename ...Args>
bool none_of(T const v, Args&&... args) requires (sizeof...(args) > 0)
{
   return ((args != v) && ...);
}

As you can see, C++ offers different standard and DIY ways of replacing some category of if statements with a function call.

2 Replies to “Express one of multiple options in a nice way”

  1. A switch can handle this, also.

    int option = …;
    switch (option) {
    case 12: // add fall through attribute if later version of C++
    case 23:
    case 42:
    std::cout << "it's a good option\n";
    break;

    default:
    std::cout << "it's a bad option\n";
    break;
    }

    Another simplification is to extract the boolean tests to set a boolean variable and use it for the condition.

    I like the 'any_of` suggestions.

    I wonder if this happens frequently enough to not use `if`.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.