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.
Higher-Order Functions
Section titled “Higher-Order Functions”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.
Functors For Flexibility
Section titled “Functors For Flexibility”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 argumentauto 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;}Guidelines
Section titled “Guidelines”- 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