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_assert
s 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.
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`.
nice one!