When we’re using an algorithm that acts on items in a collection, often we will want to apply some temporary transformation to each item first.
For example, we might have a collection of numbers, and we want to run an algorithm on their absolute values.
Or, we might have a collection of Character
objects, and we want to run an algorithm on a property of those characters, such as their name.
Scenarios like these can be elegantly solved with projection, and most of the standard library algorithms have a parameter that lets us set it up.
In the previous lesson, we introduced the 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:
std::vector Nums { -3, 0, 5, };
std::ranges::sort(Nums, std::ranges::greater{});
The std::ranges::sort
function also has an optional third parameter, which allows us to pass a projection function. In the next example, let’s pass a lambda there, to make our algorithm run on a projection instead.
In this case, we are using the default argument for the second paremter, and using the third parameter to project the integers to their absolute value, using std::abs
:
#include <vector>
#include <iostream>
#include <algorithm>
int main() {
std::vector Nums { -3, 0, 5, };
std::ranges::sort(Nums, {}, [](int i) {
return std::abs(i);
});
for (const auto& Num : Nums) {
std::cout << Num << ", ";
}
}
The values are not changed, but they were sorted based on their projection instead of their real value:
0, -3, 5,
With the sort
algorithm, projections are generally unnecessary, as we can meet our needs using appropriate comparison functions:
std::ranges::sort(Numbers, [](int a, int b){
return std::abs(a) < std::abs(b);
} );
But, not all algorithms accept comparison functions, because not all algorithms require comparisons.
Almost all range based algorithms support projection, so it's a more widely available technique, and a useful tool to be aware of.
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 Character
objects by level, using a projection function:
#include <vector>
#include <iostream>
#include <algorithm>
class Character {
public:
int Level;
std::string Name;
};
int main() {
std::vector Party {
Character {"Legolas", 49},
Character {"Gimli", 47},
Character {"Gandalf", 53}
};
std::ranges::sort(Party, {}, [](Character& Character) {
return Character.Level;
});
for (const auto& Character : Party) {
std::cout << "[" << Character.Level << "] "
<< Character.Name << "\n";
}
}
[47] Gimli
[49] Legolas
[53] Gandalf
Here, we combine both a projection and a comparison function. The projection will return an int
, and then the comparison function will compare those int
 values:
std::ranges::sort(
Party,
[](int a, int b) { return a > b; },
[](Character& Character) { return Character.Level; }
);
[53] Gandalf
[49] Legolas
[47] Gimli
Finally, the value we want to project to will often be returned from a getter, defined within our objects’ class. Where that is the case, we can simply pass a reference to that function:
#include <vector>
#include <iostream>
#include <algorithm>
class Character {
public:
Character(std::string Name, int Level) :
Name(std::move(Name)),
Level(Level) {};
std::string GetName() const { return Name; }
int GetLevel() const { return Level; }
private:
int Level;
std::string Name;
};
int main() {
std::vector Party {
Character {"Legolas", 49},
Character {"Gimli", 47},
Character {"Gandalf", 53}
};
// Sort characters in alphabetical order by name
std::ranges::sort(Party, {}, &Character::GetName);
for (const auto& Character : Party) {
std::cout << "[" << Character.GetLevel() << "] "
<< Character.GetName() << "\n";
}
}
[53] Gandalf
[47] Gimli
[49] Legolas
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.