Learn to use projection functions to apply range-based algorithms on derived data

Updated

When weâ€™re using an algorithm that acts on items in a collection, often we donâ€™t want the exact values in the collection to be theÂ input.

For example, we might have a collection of numbers, and we want to sort them by their * absolute*Â value.

Or, we have a collection of `Player`

objects, and we want to run an algorithm on a property of those objects, such as their emailÂ addresses.

To generalize the above ideas, we want a way for our algorithms to receive * projections* of the objects in our collection. Each projection is based on the original object but can have a different value, and even a differentÂ type.

To support this, almost every range-based algorithm in the `<ranges>`

library has an overload that receives a projectionÂ function.

These functions will receive objects within our collection as a parameter and will return a new object based on thatÂ parameter.

The algorithm will then use these projections to drive the behavior of theÂ algorithm.

In the previous lesson, we introduced the `std::ranges::sort()`

algorithm. Its first argument is the range we want to sort, and the optional second argument is the comparison we want to use to sort theÂ range.

In this example, we pass `{}`

as the comparison function, causing the algorithm to use the defaultÂ value:

```
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Nums{-3, 5, 0};
std::ranges::sort(Nums, {});
for (auto Num : Nums) {
std::cout << Num << ", ";
}
}
```

`-3, 0, 5,`

The `std::ranges::sort()`

function also has an optional third parameter, which is how we provide a projectionÂ function.

In the next example, we pass a function that projects the numbers in our collection to their absolute value, using `std::abs()`

:

```
#include <algorithm>
#include <iostream>
#include <vector>
int Project(int x) {
return std::abs(x);
}
int main() {
std::vector Nums{-3, 5, 0};
std::ranges::sort(Nums, {}, Project);
for (auto Num : Nums) {
std::cout << Num << ", ";
}
}
```

Now, our `std::ranges::sort()`

call has sorted our numbers by their projection - that is, their absoluteÂ value:

`0, -3, 5,`

With the `std::ranges::sort()`

algorithm, we could also have implemented this behavior by customizing the comparisonÂ functions:

```
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Nums{-3, 5, 0};
std::ranges::sort(Nums, [](int a, int b){
return std::abs(a) < std::abs(b);
});
for (auto Num : Nums) {
std::cout << Num << ", ";
}
}
```

`0, -3, 5,`

But, the comparison function is specific to `std::ranges::sort()`

- this is not a technique that we can alwaysÂ use.

Projection functions, meanwhile, are available to almost every algorithm in `std::ranges`

.

Our projection function does not need to return the same type of object that was contained in our originalÂ collection.

In this example, we sort `Player`

objects by level, using a projectionÂ function:

```
#include <vector>
#include <iostream>
#include <algorithm>
struct Player {
std::string Name;
int Level;
};
int main() {
std::vector Party {
Player {"Legolas", 49},
Player {"Gimli", 47},
Player {"Gandalf", 53}
};
std::ranges::sort(Party, {}, [](Player& P) {
return P.Level;
});
for (const auto& P : Party) {
std::cout << "[" << P.Level << "] "
<< P.Name << "\n";
}
}
```

```
[47] Gimli
[49] Legolas
[53] Gandalf
```

Here, we combine both a projection and a comparison function. The projection function will return an `int`

, and then the comparison function will compare those `int`

Â values:

```
#include <vector>
#include <iostream>
#include <algorithm>
struct Player {/*...*/}
int main() {
std::vector Party {/*...*/}
std::ranges::sort(
Party,
[](int a, int b) { return a > b; },
[](Player& P) { return P.Level; }
);
for (const auto& P : Party) {/*...*/}
}
```

```
[53] Gandalf
[49] Legolas
[47] Gimli
```

Finally, weâ€™ll often want our algorithms to receive a class member as theirÂ projection.

Where that is the case, we can simply pass a reference to that function. Below, we provide `Player::GetName`

as our projection function, causing our `Player`

objects to be sorted alphabetically byÂ name:

```
#include <vector>
#include <iostream>
#include <algorithm>
class Player {/*...*/}
int main() {
std::vector Party {
Player{"Legolas"},
Player{"Gimli"},
Player{"Gandalf"}
};
std::ranges::sort(Party, {}, &Player::GetName);
for (const auto& P : Party) {
std::cout << P.GetName() << '\n';
}
}
```

```
[53] Gandalf
[47] Gimli
[49] Legolas
```

In this lesson, we've explored how to utilize projection functions with C++'s `<ranges>`

library to manipulate and sort collections based on both their inherent and derived properties. These techniques allow for more flexible and powerful data manipulation, enhancing the versatility of range-basedÂ algorithms.

- Projection functions allow algorithms to operate on transformed or derived values from the objects in a collection.
- The
`<ranges>`

library supports projection in almost all range-based algorithms, enabling operations on both value and type-transformed projections. - Examples demonstrated sorting numbers by their absolute value and sorting objects based on a member's value, showcasing the utility of projections.
- We compared the use of projection functions with custom comparison functions, highlighting projections' broader applicability across different algorithms.
- The lesson highlighted the syntax and usage of projection functions, including how to apply them to sort collections by a property or even by a member function's return value.

Was this lesson useful?

Updated

Lesson Contents### Projection Functions

Learn to use projection functions to apply range-based algorithms on derived data

This lesson is part of theÂ course:### Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.