Skip to content

Use Named Predicates to improve readability when using algorithms

Named predicates like is_positive can improve readability and intent when working with generic algorithms, by reducing the boilerplate of lambda syntax.

const auto is_positive = [](const int value) { return value > 0; };

Higher-order functions like greater_than add more power to these named predicates by enabling input arguments to the predicates.

template <typename T>
auto greater_than(const T& other) {
return [other](const T& element) { return element > other; };
}

Named predicates and higher order functions reduce boilerplate and keep code expressive. For more advanced or type-specific predicates, functors offer greater flexibility, especially when template specialization is needed.

// Define `equals` higher order function
template <typename T> auto equals(const T &other) {
return [other](const T &element) { return element == other; };
// Usage with standard algorithm
bool found = std::any_of(
numbers.cbegin(),
numbers.cend(),
equals(3));
// Usage with C++20 Ranges
bool found = std::ranges::any_of(numbers, equals(3));

Notice how closely the C++ code resembles the natural expression: any of numbers equals (to) 3. Compared with a raw for-loop or for-each loop, this is much more concise and expressive. It is not concerned with verbose implementation or syntax details.

template <typename T>
auto has_id(int id) {
return [id](const T& element) { return element.id == id; };
}
auto object_42 = std::find_if(
objects.cbegin(),
objects.cend(),
has_id<Object>(42));

The previous snippet requires an explicit template parameter Object in has_id<Object>. This is boilerplate that distracts from the expression of intent. The need for this can be eliminated by using a functor instead of a lambda.

struct has_id {
has_id(int id) : id(id) {}
template<typename T>
bool operator()(const T& obj) { return obj.id == id; }
private:
int id;
};
struct Object { int id; };
// has_id without template argument
auto object_42 = std::ranges::find_if(objects, has_id(42));

Using a functor, we can also extend and specialize. In the following snippet, we extend has_id with a specialization for Widget, which provides access to its the id via the get_id() method.

class Widget {
public:
Widget(int id) : id(id) {}
int get_id() const { return id; }
private:
int id;
};
template <>
bool has_id::operator()(const Widget& obj) {
return obj.get_id() == id;
}

Pseudocode as expression of intent

is any of the numbers positive?

Messy C++

bool found = false;
for(int x : numbers) {
if (x > 0) {
found = true;
break;
}
}

Better

bool found = std::any_of(
numbers.cbegin(),
numbers.cend(),
[](const int x) { return x > 0; });
}

Best

using std::ranges::any_of;
bool is_any_positive = any_of(numbers, is_positive);
// Or:
bool is_any_positive = any_of(numbers, greater_than(0));
  • Prefer named predicates over inlining lambdas directly in algorithm calls. This makes code more concise and improves readability.
  • Use higher-order functions to generate predicates that take input arguments, such as equals(3) or greater_than(42).
  • Reach for functors when you need extensibility or type-specific behavior via specialization.
  • Build a reusable predicate library in your project, so common checks can be defined once and reused, without duplicating boilerplate at the call sites.