Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lambda type deduction

auto dothings = [](long position) {
auto variable;
/*do things*/
return variable;
};

float x = dothings(1l);
char  y = dothings(2l);

Basically, the thing I'm curious about, is whether is it possible in any way for the variable inside the lambda to deduce the type the return value is assigned to, in this situation it's float and char. Is there an equivalent to template typename? Thanks.

like image 928
marek Avatar asked Mar 05 '23 13:03

marek


1 Answers

This can be done, but it's a) kinda complex, and b), not really a great idea, 99.9% of the time. Here's how you proceed. The only way you can do something based on the type you assign the expression to is to take advantage of implicit conversions. This requires a templated implicit conversation operator, which can't be declared locally in a lambda, so we have to start by writing a bit of support code:

template <class T>
struct identity {
    T get(); // not defined
};


template <class F>
struct ReturnConverter {

    F f;

    template <class T>
    operator T() {
        return f(identity<T>{});
    }
};

template <class F>
auto makeReturnConverter(F f) { return ReturnConverter<F>{f}; }

The first class is just to help the lambda with inferring types. The second class is one that itself takes a lambda (or any callable), and has an implicit conversion operator to any type. When the conversion is asked for, it calls the callable, using our identity class template as a way to feed the type. We can use this now like this:

auto doIt = [] (long l) {
    return makeReturnConverter([=] (auto t) {
        return l + sizeof(decltype(t.get()));
    });
};

This lambda creates our special ReturnConverter class by feeding in another lambda. This lambda captures the long l argument of the outer lambda (by value), and it's prepared to accept our special identity class as the sole argument. It can then back out the "target" or destination type. I use sizeof here to quickly show an example where the result depends on both the argument to the lambda, and the target type. But note that once I get the type using decltype(t.get()), I could actually declare a variable of that type and do whatever I wanted with it.

float x = doIt(5);
double y = doIt(2);

After these calls, x will be 9 (a float is size 4, + 5) and y will be 10 (double is size 8, + 2).

Here's a more interesting example:

auto doIt2 = [] (long l) {

    return makeReturnConverter([=] (auto t) {
        decltype(t.get()) x;
        for (std::size_t i = 0; i != l; ++i ) {
            x.push_back(i*i);
        }
        return x;
    });
};

std::vector<int> x = doIt2(5);
std::deque<int> y = doIt2(5);

Here, I'm able to generically build a standard container according to what the left side asks for, as long as the standard container has the push_back method.

Like I said to start, this is overly complicated and hard to justify in the vast majority of cases. Usually you could just specific type as an explicit (non-inferred) template parameter and auto the left. I have used it in very specific cases though. For instance, in gtest, when you declare test fixtures, any reused data are declared as member variables in the fixture. Non static member variables have to be declared with their type (can't use auto), so I used this trick to allow quickly building certain kinds of fixture data while keeping repetition to as close to zero as I could. It's ok in this use case because that code that code doesn't need to be very robust but really wants to minimize repetition at almost any cost; usually this isn't such a good trade-off (and with auto available on the left isn't usually necessary).

like image 163
Nir Friedman Avatar answered Mar 17 '23 04:03

Nir Friedman