I am about two thirds through Scott Meyers’ Effective Modern C++, and I have discovered the power of generic lambdas. Actually I had read about generic lambdas before in the Wikipedia entry on C++14, but that was far from enough for me to get it—and I was not smart enough to investigate deeper. Anyway, Item 33 in Effective Modern C++ gives enough examples to show me the power, and it is exactly the tool I need to solve the problems in my compose
function.
Let me start from my (poor) exclamation in my last blog:
Alas, a lambda is only a function, but not a type-deducing function template.
Wrong, wrong, wrong! The generic lambda is exactly what I claimed it was not.
Type deduction was the problem that caused my compose
function template to fail. Recall its definition and the failure case:
template <typename Tp> auto compose() { return apply<Tp>; } template <typename Tp, typename Fn, typename... Fargs> auto compose(Fn fn, Fargs... args) { return [=](Tp&& x) -> decltype(auto) { return fn(compose<Tp>(args...)(forward<Tp>(x))); }; } … Obj obj(0); auto const op_nr = compose<Obj>(clone); test(op_nr(obj));
The error was that obj
, as an lvalue, could not be bound to Obj&&
(on line 10). Although I tried to use the perfect forwarding pattern, it did not work, as there was no type deduction—Tp
was specified by the caller. Why so? Because I thought a lambda could not be a type-deducing function template.
I won’t go into details about generic lambdas per se, of which you can get a lot of information in Scott’s book or by Google. Instead, I only want to show you that generic lambdas help solve the type deduction problem and give a function template to suit my needs.
Without further ado, I am showing you the improved version of compose
that uses generic lambdas:
auto compose() { return [](auto&& x) -> decltype(auto) { return forward<decltype(x)>(x); }; } template <typename Fn, typename... Fargs> auto compose(Fn fn, Fargs... args) { return [=](auto&& x) -> decltype(auto) { return fn(compose(args...)(forward<decltype(x)>(x))); }; }
You can immediately notice the following:
- The original template type parameter
Tp
is gone. Tp&&
is now changed toauto&&
, allowing type deduction to work.- In order to make perfect forwarding work when the type of
x
is unknown,forward<decltype(x)>
is used.
With this definition, We no longer need to differentiate between op_klvr
, op_rvr
, etc. No way to do so, anyway. Things are beautifully unified, until the moment you need to put it in an std::function
. Run the code at the final listing to see their differences.
Although it already looks perfect, we have a bonus since we no longer specify Tp
. We are no longer constrained by only one argument. A small change will make multiple arguments work:
auto compose() { return [](auto&& x) -> decltype(auto) { return forward<decltype(x)>(x); }; } template <typename Fn> auto compose(Fn fn) { return [=](auto&&... x) -> decltype(auto) { return fn(forward<decltype(x)>(x)...); }; } template <typename Fn, typename... Fargs> auto compose(Fn fn, Fargs... args) { return [=](auto&&... x) -> decltype(auto) { return fn( compose(args...)(forward<decltype(x)>(x)...)); }; }
The first compose
function is no longer useful when we have at least one function passed to compose
, but I am keeping it for now. The parameter pack plus the generic lambda makes a perfect combination here.
Finally, a code listing for you to play with is provided below (also available as test_compose.cpp in the zip file download for my last blog):
#include <functional> #include <iostream> using namespace std; #define PRINT_AND_TEST(x) \ cout << " " << #x << ":\n "; \ test(x); \ cout << endl; auto compose() { return [](auto&& x) -> decltype(auto) { return forward<decltype(x)>(x); }; } template <typename Fn> auto compose(Fn fn) { return [=](auto&&... x) -> decltype(auto) { return fn(forward<decltype(x)>(x)...); }; } template <typename Fn, typename... Fargs> auto compose(Fn fn, Fargs... args) { return [=](auto&&... x) -> decltype(auto) { return fn( compose(args...)(forward<decltype(x)>(x)...)); }; } struct Obj { int value; explicit Obj(int n) : value(n) { cout << "Obj(){" << value << "} "; } Obj(const Obj& rhs) : value(rhs.value) { cout << "Obj(const Obj&){" << value << "} "; } Obj(Obj&& rhs) : value(rhs.value) { rhs.value = -1; cout << "Obj(Obj&&){" << value << "} "; } ~Obj() { cout << "~Obj(){" << value << "} "; } }; void test(Obj& x) { cout << "=> Obj&:" << x.value << "\n "; } void test(Obj&& x) { cout << "=> Obj&&:" << x.value << "\n "; } void test(const Obj& x) { cout << "=> const Obj&:" << x.value << "\n "; } Obj clone(Obj x) { cout << "=> clone(Obj):" << x.value << "\n "; return x; } void test() { Obj obj(0); cout << endl; auto const op = compose(clone); std::function<Obj(const Obj&)> fn_klvr = op; std::function<Obj(Obj&&)> fn_rvr = op; std::function<Obj(Obj)> fn_nr = op; PRINT_AND_TEST(op(obj)); PRINT_AND_TEST(fn_klvr(obj)); PRINT_AND_TEST(fn_nr(obj)); cout << endl; PRINT_AND_TEST(op(Obj(1))); PRINT_AND_TEST(fn_klvr(Obj(1))); PRINT_AND_TEST(fn_rvr(Obj(1))); PRINT_AND_TEST(fn_nr(Obj(1))); } template <typename T1, typename T2> auto sum(T1 x, T2 y) { return x + y; } template <typename T1, typename T2, typename... Targ> auto sum(T1 x, T2 y, Targ... args) { return sum(x + y, args...); } template <typename T> auto sqr(T x) { return x * x; } int main() { test(); cout << endl; auto const op = compose(sqr<int>, sum<int, int, int, int, int>); cout << op(1, 2, 3, 4, 5) << endl; }
Happy hacking!
One thought on “Generic Lambdas and the
compose
Function”