Why do range algorithms have projection argument after comparator argument?

Viewed 78

C++20 range algorithms support projections, and obviously like STL algorithms they support custom comparators.

But what I found puzzling is the order of the projection and the comparator.

My "problem"(more of a annoyance, I will quickly learn to use this order of arguments) is that it breaks the left to right flow of code.

Consider the following code (apologies for it not being short, but that is kind of the point, to show that in realistic code order of arguments makes code harder to read when your variables names are not 2 letter long):

struct PointyPoint {
  int x;
  int y;
};

struct Item {
  std::string name;
  PointyPoint location;
};

//...

  std::ranges::sort(
      items,
      [](const PointyPoint &a, const PointyPoint &b) {
        return std::tie(a.x, a.y) < std::tie(b.x, b.y);
      },
      &Item::location);

Issue I have with this code is that I think it would look much nicer if projection was before the lambda (comparator).

Full code godbolt.

Reasons why I can think this order was picked:

  • STL algorithms have comparator usually as 3rd argument(first after the iterators) so it is to match that
  • maybe custom comparators are much more common that projection, so to avoid need for supplying that argument
1 Answers

I believe you have already mentioned the reasoning behing this yourself, but let me harden your points:

  • Porting pre-ranges code to <ranges> should be straightforward. Imagine you have an existing

    std::sort(data.begin(), data.end(), std::greater<>{});
    

    and would like to turn that into a <ranges> algorithm call. Imagine you had to go with

    std::ranges::sort(data, std::identity{}, std::greater<>{});
    

    This does compose a burden that hinders easy migration. With the actual ordering, you can just change the first version to

    std::ranges::sort(data, std::greater<>{});
    
  • Using the standard comparison is very low friction, so if you need a projection, it's easy to prepend a std::less<>{}. Admittedly, you could say the same thing about std::identity{}, but that doesn't rule out the first point.

Last, the rational in this document might also be helpful on that subject:

For algorithms that optionally accept functions/predicates (e.g. transform, sort), projection arguments follow functions/predicates. There are no algorithm overloads that allow the user to specify the projection without also specifying a predicate, even if the default would suffice. This is to reduce the number of overloads and also to avoid any potential for ambiguity.

Related