Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this legal template lambda syntax?

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);
}
like image 761
Simpleton Avatar asked Sep 02 '20 07:09

Simpleton


2 Answers

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.

like image 81
Jan Schultke Avatar answered Oct 09 '22 17:10

Jan Schultke


All standard references below refers to N4659: March 2017 post-Kona working draft/C++17 DIS.


Generic lambdas: a C++14 feature

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>")
}

Generic lambdas with an explicit template parameter list: a C++20 feature

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:

  • the invented template parameters can only be type template parameters synthesized (as shown above), and
  • the type template parameters cannot be directly accessed in the body of the lambda, but needs to be extracted using 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
}
like image 41
dfrib Avatar answered Oct 09 '22 19:10

dfrib