🎱
Проекции: Что такое проекция?Одна из полезнейших тем в C++ диапазонах — это проекции.
Проекция — это функция, которая «извлекает» ключ из элемента перед тем, как алгоритм с ним работает. Подавляющее большинство алгоритмов из
std::ranges принимают необязательный параметр
proj; по умолчанию там
std::identity — элемент идёт как есть.
⏰
ranges::sort — сортировка по полюВозьмём те самые задачи из
прошлого поста:
struct Task { std::string name; int priority; };
std::vector<Task> tasks = { {"Код", 1}, {"Деплой", 3}, {"Тесты", 2} };
std::ranges::sort(tasks, {}, &Task::priority);
// порядок: Код(1), Тесты(2), Деплой(3)Здесь
{} — компаратор по умолчанию (
ranges::less), а
&Task::priority — проекция. Для каждой пары алгоритм считает
less(a.priority, b.priority). Помните, в свёртках, чтобы сложить по
priority, пришлось писать лямбду? Тут поле достаёт сама проекция.
🎨
Проекция по указателю на членСамый частый случай —
&Тип::поле. И работает не только в
sort:
// найти задачу с priority == 3
auto it = std::ranges::find(tasks, 3, &Task::priority);
// *it == Task{"Деплой", 3}
// посчитать задачи с priority > 1
auto n = std::ranges::count_if(tasks,
[](int p){ return p > 1; }, &Task::priority);
// n == 2
Обратите внимание на порядок: у
find проекция идёт сразу после искомого значения (компаратора у него нет), а у
count_if — после предиката. У каждого алгоритма свой набор параметров, проекция в нём всегда последняя.
🧁
Проекцией может быть что угодно вызываемоеНе только поле. Подойдёт указатель на метод, лямбда, свободная функция — всё, что переваривает
std::invoke:
std::vector<std::string> v = {"apple", "fig", "banana"};
std::ranges::sort(v, {}, &std::string::size); // по длине
// "fig"(3), "apple"(5), "banana"(6)
std::vector<int> nums = {-5, 2, -1, 3};
std::ranges::sort(nums, {}, [](int x){ return std::abs(x); }); // по модулю
// -1, 2, 3, -5🎁
Проекция меняет ВЗГЛЯД, но не результатЭто главное, что надо уложить в голове. Проекция влияет только на то, ПО ЧЕМУ алгоритм принимает решения. Возвращает он по-прежнему исходные элементы, а не спроецированные значения:
auto top = std::ranges::max(tasks, {}, &Task::priority);
// top — это Task{"Деплой", 3}, а НЕ число 3
// (и это копия: max возвращает элемент по значению)
auto it = std::ranges::min_element(tasks, {}, &Task::priority);
// *it — Task{"Код", 1}, итератор на сам элемент (копии нет)То есть проекция — это «по какому ключу», а не «во что превратить». Если нужно именно превратить элементы — это уже
views::transform.
🍕
У бинарных алгоритмов проекций двеТам, где сравниваются два диапазона (
equal,
mismatch, …), проекций тоже две — по одной на каждый вход:
std::vector<Task> a = { {"X", 1}, {"Y", 2} };
std::vector<Task> b = { {"A", 1}, {"B", 2} };
// равны ли списки по приоритетам, игнорируя имена?
bool same = std::ranges::equal(a, b, {}, &Task::priority, &Task::priority);
// {} — предикат по умолчанию, дальше proj1 и proj2
// true: 1 == 1, 2 == 2‼️
Несколько подводных камней•
Порядок аргументов. proj идёт после компаратора, а не вместо.
sort(tasks, &Task::priority) — указатель на поле не годится в компараторы → простыня ошибок. Правильно:
sort(tasks, {}, &Task::priority).
•
Проекцию зовут много раз. В sort — на каждом сравнении, O(n log n) вызовов. Тяжёлую (парсит строку) выгоднее посчитать заранее и сортировать по готовым ключам.
•
Проекция ≠ преобразование вывода. На выходе всегда исходные элементы. Превратить их —
это views::transform.
•
Свёртки без проекций. У
fold_* параметра
proj нет (хотя у for_each и transform есть). «Свернуть по полю» — только лямбдой или через views::transform.
📍Навигация: Вакансии • Задачи • СобесыБиблиотека C/C++ разработчика#константная_правильностьx