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

Updated

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

For example, we might have a collection of numbers that we want to sort 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 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 function to use for sorting 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 often want our algorithms to use a class member as theirÂ projection.

In such cases, we can simply pass a reference to that function. Below, we use `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 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 how 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.