More ergonomic / convenient STL algorithm calls -- sort order & member field extraction using std::invoke While we patiently wait for ranges in C++20, it can still get somewhat frustrating to write begin(), end() and micro-lambdas to specify "sort order" and "field extraction". 

It is the latter, "field extraction" which I find is very commonly needed, but rarely discussed. The C++11 lambda has made it easier but the syntax is still very verbose for a very common task. There was even a recent slack/cpplang question about it. Inspired by a comment in Sean Parent's famous Seasoning talk, where he suggested using std::bind to abstract this syntax, I looked into modern alternatives. std::bind has a bad name for being large and slow. More modern is std::invoke from C++17. 

So my proof of concept below tries to get to some terser syntax for these common operations using std::invoke. 

I am looking for feedback on:


Coding style
Constraining that template - with / without c++20 concepts (it's wide open right now)
The new, terser calling syntax for STL algos which operate on some user defined class and require field/member extraction and potentially sorting on those. 
std::sort is just one example. This could work for std::accumulate or anything really. Would I need up to 115 such wrapper templates? is there a better way. 


Code size may still be a concern (even with std::invoke rather than std::bind) as shown by the the generated ASM here. The base case of the traditional lambda generates ~2000 lines of assembly code (LOA). Then each of the "new syntax" lines generates a further 2000 LOA -bringin it to ~8000 LOA in total for clang-9 -O3. Because the sortby template is stamped out for each type of lambda. 

I haven't measured performance. Will all this code slow it down significantly with a lot of boilerplate and indirection, or is it just code size that suffers? 

EDIT: Benchmark data has now been added below.

#include &lt;algorithm&gt;
#include &lt;functional&gt;
#include &lt;iostream&gt;
#include &lt;ostream&gt;
#include &lt;string&gt;
#include &lt;vector&gt;

struct Employee {
  int         id;
  std::string firstname;
  std::string lastname;
  int         yob;

  [[nodiscard]] int getAge() const noexcept { return 2020 - yob; }

  friend std::ostream&amp; operator&lt;&lt;(std::ostream&amp; stream, const Employee&amp; e) {
    return stream &lt;&lt; e.id &lt;&lt; ", " &lt;&lt; e.firstname &lt;&lt; ", " &lt;&lt; e.lastname &lt;&lt; ", " &lt;&lt; e.yob &lt;&lt; ", "
                  &lt;&lt; e.getAge();
  }
};

namespace os::algo {

// the following 6 lines make it possible
template &lt;typename Container, typename Member, typename Comparison = std::less&lt;&gt;&gt;
void sortby(Container&amp; c, Member&amp;&amp; m, Comparison comp = Comparison()) {
  std::sort(c.begin(), c.end(), [&amp;m, &amp;comp](const auto&amp; a, const auto&amp; b) {
    return comp(std::invoke(m, a), std::invoke(m, b));
  });
}

} // namespace os::algo

int main() {

  using os::algo::sortby;

  auto employees = std::vector&lt;Employee&gt;{
      {1, "James", "Smith", 2008},
      {2, "John", "Jones", 1998},
      {3, "Sarah", "Evans", 1968},
      {4, "Michelle", "Harris", 1978},
  };

  // BEFORE: This is what's required with normal STL calls
  std::sort(employees.begin(), employees.end(),
            [](const auto&amp; a, const auto&amp; b) { return a.firstname &lt; b.firstname; });

  // AFTER: and now we can do this...

  // the easy part -- no begin(), end()

  // trivial, terse sorting by any member field
  sortby(employees, &amp;Employee::firstname);

  // trivially specify sort direction
  sortby(employees, &amp;Employee::yob, std::greater&lt;&gt;());

  // can use getters too
  sortby(employees, &amp;Employee::getAge, std::greater&lt;&gt;());

  for (const auto&amp; e: employees) std::cout &lt;&lt; e &lt;&lt; "\n";

  // std::sort is but one example. The same technique applies to any STL algorithm

  return 0;
}
Indeed, your sortby when used in the style shown, with data members, will not be quite as fast as if you used the STL with a lambda. Look at the difference in assembly between

struct Date {
    int year, month, day;
};

void test1(std::array&lt;Date, 100&gt;&amp; a) {
    sortby(a, &amp;Date::month);
    sortby(a, &amp;Date::year);
}

void test2(std::array&lt;Date, 100&gt;&amp; a) {
    sortby(a, [](const auto&amp; x){ return x.month; });
    sortby(a, [](const auto&amp; x){ return x.year; });
}


In test1, the difference between &amp;Date::month and &amp;Date::year is encoded in the runtime value of the member pointer. In test2, the difference is encoded in the compile-time type of the lambda. So test2 instantiates two different template instantiations, whereas test1 calls the same instantiation twice with different inputs. (But if both calls are inlined, the optimizer might be able to hide the difference.)

However, this is not the fault of your sortby helper; it's the fault of using member pointers instead of custom lambdas. You can observe the same differences happening if you call std::sort with a function pointer, versus calling std::sort with an instance of some bespoke type such as std::less&lt;&gt; or std::greater&lt;&gt;.



You pass Member&amp;&amp; m by forwarding reference, but then you use it as an lvalue (without forwarding it again). I think I've mentioned before: if you don't intend to forward an argument, then you shouldn't be using a forwarding reference with it. In this case, you just need to look at the value of the member-pointer m without modifying it, so const Member&amp; m would be appropriate. Or, since member data pointers and custom lambdas are both going to be small trivial types, and member function pointers are also trivial types (if slightly larger), we should seriously think about taking Member m directly by value.

Notice that you already take Comparison comp by value.

The comment about using std::begin(c) over c.begin() is accurate. If you want your sortby template to work for C-style arrays, then you should use std::begin(c). Remember, you can't pass arrays by value (they decay to pointers) — but you can certainly pass arrays by reference, and that's what you're doing here when you sortby an array "in place."

The vast majority of lambdas you write should just take [&amp;]. I don't see any benefit to writing [&amp;m, &amp;comp] when it ends up meaning the same thing as [&amp;].

So I'd write your helper like this:

template&lt;class Container, class Member, class Comparison = std::less&lt;&gt;&gt;
void sortby(Container&amp; c, Member m, Comparison comp = Comparison()) {
    using std::begin; using std::end;
    std::sort(begin(c), end(c), [&amp;](const auto&amp; a, const auto&amp; b) {
        return comp(std::invoke(m, a), std::invoke(m, b));
    });
}


using std::begin; is the "std::swap two-step," familiar to anyone who's used std::swap before. It means "consider using std::begin for unqualified calls, but if someone has provided a better-matching ADL version of begin, then we'll happily use that version instead." This is certainly overkill — I mean we could just write std::begin(c), std::end(c), — but the two-step is how all the standard customization points are meant to be used as far as I know, so we might as well stick to it.

Vice versa, I would personally shy away from using the two-step in main —

using os::algo::sortby;


This seems more confusing than helpful. Are you expecting that someone else might provide a better-matching ADL sortby? If not, then sortby(x,y,z) will always mean os::algo::sortby(x,y,z)... and if it is os::algo::sortby, why not say so?