While refactoring some legacy code, I came across this traditional implementation of a predicate to be used in STL algorithms:
template<bool b>
struct StructPred {
bool operator()(S const & s) { return s.b == b; }
};
I was tired and hitting the Ballmer Peak, so I accidentally rewrote it into a lambda like this, which seemed natural and also worked:
template<bool b>
auto lambda_pred = [] (S const & s) { return s.b == b; };
Later I realized that I've never seen a template lambda like this. I couldn't find anything similar on cppreference or on stackoverflow. The canonical way of producing template lambdas seems to be wrapping them in template structs or template functions. C++20 introduces named template params for lambdas, but that's a different syntax (after the capture brackets).
Now my questions are: Is this legal syntax? Is it documented anywhere? Is it even a lambda or something else? Are there any implications or side effects as compared to the wrapper alternatives? Why does everybody recommend wrapper implementations when this works? Am I missing something obvious?
Full working test code below and at godbolt. Just to be sure I also added a type template parameter version. MSVC, GCC and clang are happy with this code.
#include <vector>
#include <algorithm>
struct S {
bool b = false;
};
// classic function object
template<bool b>
struct StructPred {
bool operator()(S const & s) { return s.b == b; }
};
// template function producing a lambda
template<bool b>
auto make_pred() {
return [] (S const & s) { return s.b == b; };
}
// direct template lambda
template<bool b>
auto lambda_pred = [] (S const & s) { return s.b == b; };
// also with type params
template<typename T, bool b>
auto lambda_pred_t = [] (T const & t) { return t.b == b; };
std::pair<size_t, size_t> count1(std::vector<S> const & v) {
return {
std::count_if(v.begin(), v.end(), StructPred<true>{}),
std::count_if(v.begin(), v.end(), StructPred<false>{})
};
}
std::pair<size_t, size_t> count2(std::vector<S> const & v) {
return {
std::count_if(v.begin(), v.end(), make_pred<true>()),
std::count_if(v.begin(), v.end(), make_pred<false>())
};
}
std::pair<size_t, size_t> count3(std::vector<S> const & v) {
return {
std::count_if(v.begin(), v.end(), lambda_pred<true>),
std::count_if(v.begin(), v.end(), lambda_pred<false>)
};
}
std::pair<size_t, size_t> count4(std::vector<S> const & v) {
return {
std::count_if(v.begin(), v.end(), lambda_pred_t<S, true>),
std::count_if(v.begin(), v.end(), lambda_pred_t<S, false>)
};
}
void test() {
std::vector<S> v{3};
v[1].b = true;
// all implementations correctly return {1,2}
auto c1 = count1(v);
auto c2 = count2(v);
auto c3 = count3(v);
auto c4 = count4(v);
}
template<bool b>
auto lambda_pred = [] (S const & s) { return s.b == b; };
This is not really a template-lambda, it is rather a variable template which is assigned to a lambda.
It is not equivalent to adding template parameters to the implicitly declared Closure struct
which has this lambda as a call operator (the traditional approach):
template<bool b>
struct StructPred { // NOT equivalent to this
bool operator()(S const & s) { return s.b == b; }
};
struct StructPred { // NOT equivalent to this either
template<bool b>
bool operator()(S const & s) { return s.b == b; }
};
It is instead equivalent to creating different Closures depending on the template parameters of the variable. So for the bool
example, this would be like choosing between the operator()
of one of the following types:
struct StructPred_true {
bool operator()(S const & s) { return s.b == true; }
}
struct StructPred_false {
bool operator()(S const & s) { return s.b == false; }
}
This approach won't allow for partial specializations and is thus less powerful.
Another reason why this approach might be unpopular is that it doesn't give you easy access to the Closure type(s).
StructPred
can be worked with explicitly, unlike the anonymous classes StructPred_true
and StructPred_false
A template lambda in C++20 would look as follows:
auto lambda = []<bool b>(S const & s){ return s.b == b; };
This is instead equivalent to making the Closure's operator()
templated.
All standard references below refers to N4659: March 2017 post-Kona working draft/C++17 DIS.
The canonical way of producing template lambdas seems to be wrapping them in template structs or template functions. C++20 introduces named template params for lambdas, but that's a different syntax (after the capture brackets).
The other answer thoroughly explains what construct the OPs variable template is, whereas this answer addresses the emphasized segment above; namely that generic lambdas is a language feature as of C++14, and not something that is available only as of C++20.
As per [expr.prim.lambda.closure]/3 [extract]:
[...] For a generic lambda, the closure type has a public inline function call operator member template whose template-parameter-list consists of one invented type template-parameter for each occurrence of
auto
in the lambda's parameter-declaration-clause, in order of appearance. [...]
a generic lambda can be declared as
auto glambda = [](auto a, auto b) { return a < b; };
which is comparable to
struct anon_struct {
template<typename T, typename U>
bool operator()(T a, U b) { return a < b; }
}
and not
template<typename T, typename U>
struct anon_struct {
bool operator()(T a, U b) { return a < b; }
}
which is essential as a single generic lambda object (whose closure type is in fact not a class template but a non-template (non-union) class) can be used to generically invoke its function call operator template for different instantiations of its invented template parameters.
#include <iostream>
#include <ios>
int main() {
auto gl = [](auto a, auto b) { return a < b; };
std::cout << std::boolalpha
<< gl(1, 2) << " " // true ("<int, int>")
<< gl(3.4, 2.2) << " " // false ("<double, double>")
<< gl(98, 'a'); // false ("<int, char>")
}
As of C++20 we may use an explicit template parameter list when declaring the generic lambdas, as well as offering a sugared syntax for providing explicit template arguments when invoking the generic lambdas.
In C++14 and C++17 the template parameters for a generic lambda can only be declared implicitly as invented type template parameters for each declared auto
parameter in the lambda declaration, which has the restrictions that:
decltype
on the respective auto
parameter.Or, as shown with a contrived example:
#include <type_traits>
// C++17 (C++14 if we remove constexpr
// and use of _v alias template).
auto constexpr cpp17_glambda =
// Template parameters cannot be declared
// explicitly, meaning only type template
// parameters can be used.
[](auto a, auto b)
// Inventend type template parameters cannot
// be accessed/used directly.
-> std::enable_if_t<
std::is_base_of_v<decltype(a), decltype(b)>> {};
struct Base {};
struct Derived : public Base {};
struct NonDerived {};
struct ConvertsToDerived { operator Derived() { return {}; } };
int main() {
cpp17_glambda(Base{}, Derived{}); // Ok.
//cpp17_glambda(Base{}, NonDerived{}); // Error.
// Error: second invented type template parameter
// inferred to 'ConvertsToDerived'.
//cpp17_glambda(Base{}, ConvertsToDerived{});
// OK: explicitly specify the types of the invented
// type template parameters.
cpp17_glambda.operator()<Base, Derived>(
Base{}, ConvertsToDerived{});
}
Now, in C++20, with the introduction of name template parameters for lambdas (as well as requires clauses), the example above can be reduced to:
#include <type_traits>
auto constexpr cpp20_glambda =
[]<typename T, typename U>(T, U)
requires std::is_base_of_v<T, U> { };
struct Base {};
struct Derived : public Base {};
struct NonDerived {};
struct ConvertsToDerived { operator Derived() { return {}; } };
int main() {
cpp20_glambda(Base{}, Derived{}); // Ok.
//cpp20_glambda(Base{}, NonDerived{}); // Error.
// Error: second type template parameter
// inferred to 'ConvertsToDerived'.
//cpp20_glambda(Base{}, ConvertsToDerived{});
// OK: explicitly specify the types of the
// type template parameters.
cpp20_glambda.operator()<Base, Derived>(
Base{}, ConvertsToDerived{});
}
and we can moreover declare lambdas with template parameters that are not necessarily type template parameters:
#include <iostream>
#include <ios>
template<typename T>
struct is_bool_trait {
static constexpr bool value = false;
};
template<>
struct is_bool_trait<bool> {
static constexpr bool value = true;
};
template<typename T>
struct always_true_trait {
static constexpr bool value = true;
};
int main() {
auto lambda = []<
template<typename> class TT = is_bool_trait>(auto a) -> bool {
if constexpr (!TT<decltype(a)>::value) {
return true; // default for non-bool.
}
return a;
};
std::cout << std::boolalpha
<< lambda(false) << " " // false
<< lambda(true) << " " // true
<< lambda(0) << " " // true
<< lambda(1) << " " // true
<< lambda.operator()<always_true_trait>(0) << " " // false
<< lambda.operator()<always_true_trait>(1); // true
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With