Projections with Ranges

The algorithms of the ranges library are lazy, can work directly on the container, and can easily be composed. But they have more to offer: projections. A projection is a mapping of a set into a subset. Let me show you in this post what that means:

I ended my last post, “The Ranges Libary in C++20: More Details” with a comparison of std::sort and std::ranges::sort. Here are the two overloads of std::ranges::sort:

 
template <std::random_access_iterator I, std::sentinel_for<I> S,
         class Comp = ranges::less, class Proj = std::identity>
requires std::sortable<I, Comp, Proj>
constexpr I sort(I first, S last, Comp comp = {}, Proj proj = {});

template <ranges::random_access_range R, class Comp = ranges::less, 
          class Proj = std::identity>
requires std::sortable<ranges::iterator_t<R>, Comp, Proj>
constexpr ranges::borrowed_iterator_t<R> sort(R&& r, Comp comp = {}, Proj proj = {});
When you study the first overload, you notice that it takes a sortable range R, a predicate Comp, and a projection Proj. The predicate Comp uses for default ranges::less, and the projection Proj the identity std::identity that does return its arguments unchanged. std::identity have been added with C++20, a function object defined in the header <functional>.
In short, here are the components:
  • Comparators: Comp (binary functions that return a boolean)
  • Projections: Proj (mapping of a set into a subset)
  • Sentinel: std::sentinel_for<I> (a unique value that indicates  the end of a sequence)
  • Concepts: std::random_access_iterator, std::sortable<I, Comp, Proj>, and std::sentinel_for<I>

In contrast, the second overload does not return an Iterator I, but a ranges::borrowed_iterator_t. Of course, this is also a concept and guarantees that the returned iterator is safe to use afterward. Consequentially, we call this iterator a safe iterator. I will write more about std::ranges::borrowed_iterator_t in an upcoming post.

A projection is a mapping of a set into a subset. What does this mean?

Projection

The algorithms of the ranges library operate directly on the container. This is due to the fact, that the projection is by default std::identity.
In the following example, I apply a projection to the data type PhoneBookEntry.

// rangeProjection.cpp

#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>
 
struct PhoneBookEntry{                                                   // (1)
    std::string name;
    int number;
};

void printPhoneBook(const std::vector<PhoneBookEntry>& phoneBook) {
    for (const auto& entry: phoneBook) std::cout << "(" << entry.name << ", " 
                                                        << entry.number << ")";
    std::cout << "\n\n";
}
 
int main() {

    std::cout << '\n';

    std::vector<PhoneBookEntry> phoneBook{ {"Brown", 111}, {"Smith", 444}, 
    {"Grimm", 666}, {"Butcher", 222}, {"Taylor", 555}, {"Wilson", 333} };

    std::ranges::sort(phoneBook, {}, &PhoneBookEntry::name);   // ascending by name (2)
    printPhoneBook(phoneBook);

    std::ranges::sort(phoneBook, std::ranges::greater() , 
                      &PhoneBookEntry::name);                  // descending by name (3)
    printPhoneBook(phoneBook);

    std::ranges::sort(phoneBook, {}, &PhoneBookEntry::number); // ascending by number (4)
    printPhoneBook(phoneBook);

     std::ranges::sort(phoneBook, std::ranges::greater(), 
                       &PhoneBookEntry::number);              // descending by number (5)
    printPhoneBook(phoneBook);
    
}

phoneBook (line 1) has structs of type PhoneBookEntry (line 1). A PhoneBookEntry consists of a name and a number. Thanks to projections, the phoneBook can be sorted in ascending order by name (line 2), descending order by name (line 3), ascending order by number (line 4), and descending order by number (line 5). The empty curly braces in the expression std::ranges::sort(phoneBook, {}, &PhoneBookEntry::name) cause the default construction of the sorting criteria that is, in this case, std::less.

rangeProjection

Rainer D 6 P2 500x500

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    You can use a callable such as a lambda expression when your projection is more demanding.

    // rangeProjectionCallable.cpp
    
    #include <algorithm>
    #include <functional>
    #include <iostream>
    #include <vector>
     
    struct PhoneBookEntry{ 
        std::string name;
        int number;
    };
    
    void printPhoneBook(const std::vector<PhoneBookEntry>& phoneBook) {
        for (const auto& entry: phoneBook) std::cout << "(" << entry.name << ", " 
                                                            << entry.number << ")";
        std::cout << "\n\n";
    }
     
    int main() {
    
        std::cout << '\n';
    
        std::vector<PhoneBookEntry> phoneBook{ {"Brown", 111}, {"Smith", 444}, 
        {"Grimm", 666}, {"Butcher", 222}, {"Taylor", 555}, {"Wilson", 333} };
    
        std::ranges::sort(phoneBook, {}, &PhoneBookEntry::name);                     // (1)
        printPhoneBook(phoneBook);
    
        std::ranges::sort(phoneBook, {}, [](auto p){ return p.name; } );             // (2)
        printPhoneBook(phoneBook);
    
        std::ranges::sort(phoneBook, {}, [](auto p) {                                // (3)
            return std::to_string(p.number) + p.name; 
        });
        printPhoneBook(phoneBook);
    
        std::ranges::sort(phoneBook, [](auto p, auto p2) {                           // (4)
            return std::to_string(p.number) + p.name < 
                   std::to_string(p2.number) + p2.name; 
        });
        printPhoneBook(phoneBook);
        
    }
    
    std::ranges::sort in line (1) uses the attribute PhoneBookEntry::name as a projection. Line (2) shows the equivalent lambda expression [](auto p){ return p.name; } as projection. The projection in line (3) is more demanding. It uses the stringified number concatenated with the p.name. Of course, you can use the concatenated stringified number and the name directly as sorting criteria. In this case, the algorithm call in line (3) is easier to read than in line (4). I want to emphasize this. Line (3) uses a projection as a sorting criterion, but line (4) is parametrized std::ranges::sort with a binary predicate given by the lambda expression.
    rangeProjectionCallable
    Most ranges algorithms support projections.

    What’s next?

    In my next post, I will write about sentinels. They specify the end of a range and can be regarded as generalized end iterators.

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *