While we patiently wait for ranges
in C++20, it can still get somewhat frustrating to write begin()
, end()
and micro-lambda
s 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 <algorithm>
#include <functional>
#include <iostream>
#include <ostream>
#include <string>
#include <vector>
struct Employee {
int id;
std::string firstname;
std::string lastname;
int yob;
[[nodiscard]] int getAge() const noexcept { return 2020 - yob; }
friend std::ostream& operator<<(std::ostream& stream, const Employee& e) {
return stream << e.id << ", " << e.firstname << ", " << e.lastname << ", " << e.yob << ", "
<< e.getAge();
}
};
namespace os::algo {
// the following 6 lines make it possible
template <typename Container, typename Member, typename Comparison = std::less<>>
void sortby(Container& c, Member&& m, Comparison comp = Comparison()) {
std::sort(c.begin(), c.end(), [&m, &comp](const auto& a, const auto& b) {
return comp(std::invoke(m, a), std::invoke(m, b));
});
}
} // namespace os::algo
int main() {
using os::algo::sortby;
auto employees = std::vector<Employee>{
{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& a, const auto& b) { return a.firstname < b.firstname; });
// AFTER: and now we can do this...
// the easy part -- no begin(), end()
// trivial, terse sorting by any member field
sortby(employees, &Employee::firstname);
// trivially specify sort direction
sortby(employees, &Employee::yob, std::greater<>());
// can use getters too
sortby(employees, &Employee::getAge, std::greater<>());
for (const auto& e: employees) std::cout << e << "\n";
// std::sort is but one example. The same technique applies to any STL algorithm
return 0;
}
std::begin(c)
overc.begin()
. \$\endgroup\$Container
is really aContainer<T>
b) you can callbegin/end
onContainer
and c)std::invoke(Member, T)
gives me some typeF
(the field type), which when you callComparison(F,F)
you get abool
. And throw readable compile errors (or ignore the template) if any of the above are not true. \$\endgroup\$std::begin()
andstd::end()
is that they allow iteration over containers that don't implement the class member functionsbegin()
andend()
, such as regular C-style arrays. You can overloadstd::begin()
andstd::end()
to provide iteration over classes that don't provide that functionality natively. \$\endgroup\$std::begin
. Makes. sense. But I shouldn't change the code above right ;-) ? Or because there are no answers yet, I should? Wouldn't want to upset any community police. \$\endgroup\$