marek
marek

Reputation: 53

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.

Upvotes: 4

Views: 689

Answers (3)

Loki Astari
Loki Astari

Reputation: 264331

The answer here is NO.

The type returned is based on the values used inside the function

auto dothings = [](long position) {
auto variable;        // This is illegal as the compiler can not deduce the type.
                      // An auto declaration needs some way for it to determine the type.
                      // So that when you use it below it can determine the
                      // return type of the function.
/*do things*/
return variable;
};

The assignment operator looks at the type of the result expression to see if there are any auto conversions that can be applied to convert the function result type into the destination of the assignment type.

char x = dothings(10); // the result of the expression on the right
                       // is converted to char before assignment. If
                       // it can't be converted then it is a compiler error.

You can think of lambdas as syntactic sugar for creating a functor.

[<capture List>](<Parameter List>) {
    <Code>
}

Is the same as:

struct <CompilerGeneratedName>
{
    <Capture List With Types>;
    Constructor(<Capture List>)
         : <Capture List With Types>(<Capture List>)
    {}
    <Calculated Return> operator()(<Parameter List>) const {
        <Code>
    }
}{<Capture List>};

Example:

{
    int y = 4;
    auto l1 = [&y](int x){return y++ + x;}
    struct MyF
    {
        int& y;
        MyF(int& y)
            : y(y)
        {}
        int operator()(int x) const {
            return y++ + x;
        }
    };
    auto l2 = MyF(y);

    std::cout << l2(5) << " " << l1(5) << "\n";
}

Upvotes: 3

Nir Friedman
Nir Friedman

Reputation: 17704

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).

Upvotes: 4

Paul Belanger
Paul Belanger

Reputation: 2434

The only way I know to do this is to use a template parameter:

template<typename T> 
T dothings(long value)
{
    T result;
    ...
    return result;
}

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

The template also allows you to specialize for certain types. So if you wanted different logic for doubles, you could write the following:

template<>
double dothings<double>(long value)
{
    T result;
    ...
    return result * 2.0;
}

Upvotes: 0

Related Questions