3

I root caused a problem in my code to mismatched projections in std::ranges::partial_sort_copy. After reading cppreference, I couldn't think of a legitimate situation where proj1 & proj2 needed to be different.

What's the motivation for this algorithm providing two different projections?

Aamir
  • 1,974
  • 1
  • 14
  • 18
MarkB
  • 672
  • 2
  • 9

1 Answers1

3

You need it when there is a mismatch between the type of the input range, and the type of the output range. Consider the following example:

#include <string>
#include <ranges>
#include <algorithm>
#include <iostream>

int main() {
    std::string names[] = {
        "alpha",
        "beta",
        "tau",
        "pi",
        "omega"
    };
    std::string_view shortest_three[3];

    std::ranges::partial_sort_copy(names, shortest_three,
                                   {},                         // std::ranges::less 
                                   &std::string::length,       // proj1
                                   &std::string_view::length); // proj2
    // note: It might not be allowed to take the address of
    //       member functions in std::string/std::string_view.
    //       (unspecified behavior, see [namespace.std]/6)
    //       To be on the safe side, we would have to wrap access to length()
    //       in a lambda expression.

    for (auto view : shortest_three) {
        std::cout << view << '\n';
    }
}

This outputs:

pi
tau
beta

In this example, it would be wasteful if the destination range contained std::string objects, because we could just as well store the shortest three strings as a const char* or std::string_view. As a result, there are two different projections applied here.

Technically you could always create a single projection that covers both cases:

auto proj = []<typename T>(const T& x) {
    // note: in this specific case, we don't need this if-statement,
    //       because the name of the member function is the exact same for
    //       both types. However, this may not always be the case, e.g.
    //       we need .length() for one type and .size() for another.
    if constexpr (std::is_same_v<T, std::string>) {
        return x.length();
    }
    else {
        return x.length();
    }
};

However, providing two separate projections is often more concise.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • "It actually isn't allowed to take the address of member functions in std::string/std::string_view"!?! I regularly used pointer to member functions in place of lambdas (and I believe cppreference contains examples that use this as well). Where is this restriction documented? – MarkB Jun 21 '23 at 16:15
  • @MarkB I've misremembered something and updated the answer. I was referring to: https://eel.is/c++draft/namespace.std#6. As it turns out, this restriction only applies to free functions, static member functions, and function templates, not to a non-static member function like `length`. – Jan Schultke Jun 21 '23 at 16:22
  • This is UB, and both projectors can be `ranges::size`. – 康桓瑋 Jun 21 '23 at 17:11
  • @康桓瑋 how is it UB exactly? And yes, they have a common interface that can be used in *this particular case*, but you could easily imagine that you may want to output to an array of `const char*` that you pass to a C function. – Jan Schultke Jun 21 '23 at 18:57
  • @JanSchultke I'm confused: the cited paragraph clearly also bans forming a pointer to non-static member function. Am I misreading something? – j6t Jun 21 '23 at 20:39
  • @j6t no, I misread and missed the part where it also includes *standard library non-static member functions* – Jan Schultke Jun 21 '23 at 20:52