Projections in Modern C++ Ranges Explained
Projections in modern C++ ranges algorithms are callable objects that
transform or extract data from an element before the algorithm processes
it. Introduced in C++20 as part of the <ranges> and
<algorithm> libraries, projections fundamentally
streamline element access by removing boilerplate comparator code,
reducing verbose lambdas, and separating the mechanics of data retrieval
from core algorithmic logic.
The Problem Before Projections
Prior to C++20, operating on specific attributes of complex objects required custom predicates or comparators. For instance, sorting a collection of objects by a specific member variable demanded a custom lambda:
struct User {
int id;
std::string name;
};
std::vector<User> users = /* ... */;
// Pre-C++20: Custom lambda defines the comparison logic
std::sort(users.begin(), users.end(), [](const User& a, const User& b) {
return a.id < b.id;
});This pattern forced developers to rewrite binary comparison logic
(<, ==, etc.) repeatedly, increasing the
risk of subtle bugs such as violating strict weak ordering.
What is a Projection?
A projection is an invocable entity—such as a lambda, function
pointer, or pointer-to-member—passed as an optional argument to a
constrained algorithm in std::ranges. The algorithm
evaluates the projection on each element before applying its predicate,
comparator, or action:
Algorithm Operation -> Predicate(Projection(Element))
In algorithms with binary operations (like
std::ranges::sort), the projection is applied to both
operands individually before they are evaluated by the comparison
function.
How Projections Transform Element Access
Projections dramatically simplify how elements and their internal attributes are accessed during algorithmic passes:
1. Direct Pointer-to-Member Syntax
C++20 ranges treat member pointers as callable projections via
std::invoke. You no longer need to write a lambda just to
access an object's member variable or getter function:
// Sort users by ID using standard std::less{} and a member pointer projection
std::ranges::sort(users, {}, &User::id);
// Sort users by name in descending order
std::ranges::sort(users, std::ranges::greater{}, &User::name);2. Decoupling Access from Operation
Projections decouple what attribute is being evaluated from how it is evaluated. The comparator handles ordering logic, while the projection handles property extraction:
// Check if any user has an empty name
bool has_empty = std::ranges::any_of(users, &std::string::empty, &User::name);In this example, &User::name extracts the string
member, and &std::string::empty evaluates whether that
extracted string is empty.
3. Modifying Views Without Modifying Data
Projections allow on-the-fly transformations without allocating
temporary containers or altering the underlying data. For instance,
calculating distances, casting types, or hashing elements can be
projected directly into algorithms such as
std::ranges::lower_bound or
std::ranges::min_element:
auto shortest_name = std::ranges::min_element(users, {}, [](const User& u) {
return u.name.length();
});Performance and Guarantees
Projections introduce no runtime performance overhead compared to
handwritten lambdas. Constrained algorithms forward projected arguments
using std::invoke, allowing modern compilers to completely
inline member accesses and functional mappings. Projections provide an
expressive, type-safe, and zero-cost abstraction for manipulating data
collections in modern C++.