Skip to content

Use Named Predicates to improve readability when using algorithms

Consider the question: is any of the numbers positive?

Messy C++

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

Better — a standard algorithm eliminates the loop, but inlining a lambda still buries the intent in syntax:

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

Best — a named predicate brings the code closest to the original expression of intent:

const auto is_positive = [](const int value) { return value > 0; };
using std::ranges::any_of;
bool is_any_positive = any_of(numbers, is_positive);

Named predicates reduce lambda boilerplate and keep algorithm calls expressive. Higher-order functions extend this by accepting arguments, so predicates can be generated at the call site:

template <typename T>
auto greater_than(const T& other) {
return [other](const T& element) { return element > other; };
}
bool is_any_positive = std::any_of(numbers.cbegin(), numbers.cend(), greater_than(0));
template <typename T>
auto greater_than(const T& other) {
return [other](const T& element) { return element > other; };
}
bool is_any_positive = std::ranges::any_of(numbers, greater_than(0));

For more advanced or type-specific predicates, functors offer the same expressiveness with greater flexibility through template specialization.

The same pattern applies to other predicates, such as equals:

template <typename T> auto equals(const T &other) {
return [other](const T &element) { return element == other; };
}
bool found = std::any_of(
numbers.cbegin(),
numbers.cend(),
equals(3));
template <typename T> auto equals(const T &other) {
return [other](const T &element) { return element == other; };
}
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.

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;
}
  • Prefer named predicates over inline lambdas in algorithm calls
  • Use higher-order functions to generate predicates that accept arguments
  • Reach for functors when you need extensibility or type-specific behavior via template specialization
  • Build a reusable predicate library — define common checks once and reuse them across call sites, rather than duplicating lambda boilerplate