Can I get return type auto to work with lambdas of the same signature but different captures?

Viewed 127

I'm trying to use auto as the return type for a returned lambda function. Here's a minimal example that demonstrates a problem I'm having with this:

#include <iostream>
#include <memory>

auto
get_func()
{
    auto i = std::make_unique<int>(2);
    if (*i == 1) {
        return [i=std::move(i)]() {
            return *i;
        };
    }
    return [](){ return 2; };
}

int
main(int argc, char* argv[])
{
    auto func = get_func();
    std::cout << "val: " << func() << std::endl;
    return 0;
}

On my mac, I get the following compile error:

$ g++ -g -Wall -Werror -std=c++17 test.cc -o test
test.cc:13:5: error: 'auto' in return type deduced as '(lambda at test.cc:13:12)' here but deduced as '(lambda at test.cc:9:16)' in earlier return statement
    return [](){ return 2; };
    ^
1 error generated.

Right, they're both deduced as lambda. And they both have the same void(void) signature. Am I seeing problems because they specify different captures? If so, what are my options for getting the same function call for the caller to work (as seen in the cout call in main).

3 Answers

Am I seeing problems because they specify different captures?

You would have seen a problem even if they were exactly the same, token for token.

Each lambda expression creates a unique closure type, which is different from any closure type created by any other lambda. There is no reconciling this difference, so auto deduction cannot succeed.

You'll need to type erase the actual functor if you intend to return two different lambdas (either with std::function, or a custom type that supports move only semantics). That, or perhaps roll the entire logic into a single lambda:

auto
get_func()
{
    auto i = std::make_unique<int>(2);
    return [i=std::move(i)]() {
        if (*i == 1) {
            return *i;
        }
        return 2;
    };
}

And they both have the same void(void) signature.

While the lambda's have the same operator() signature, they are not the same class type. They are distinct objects with distinct types. Auto return type deduction requires that all return statements have the same type, which you don't have here.

In this case where you have different types you need a common return type. You can get that with std::function as it lets you return by the operator() type instead of the lambda type. That gives you

std::function<void(void)> get_func()
{
    auto i = std::make_unique<int>(2);
    if (*i == 1) {
        return [i=std::move(i)]() {
            return *i;
        };
    }
    return [](){ return 2; };
}

I think the best variant (foreshadowing pun intended) is StoryTeller's solution to put the logic inside a single lambda.

For fun, as an alternative, you can use a variant to hold your lambda. And you can create a simple wrapper around it:

template <class... Fs>
struct Lambda_fixed_variant
{
    std::variant<Fs...> f_;

    template <class L>
        // requires std::is_constructible_v<decltype(f_), std::in_place_type_t<L>, L&&>
    Lambda_fixed_variant(L l)
        : f_{std::in_place_type_t<L>{}, std::move(l)}
    {}

    template <class... Args>
    auto operator()(Args&&... args) const
    {
        return std::visit([&] (const auto& l) {
                return l(std::forward<Args>(args)...);
            },
            f_
            );
    }
};

auto get_func()
{
    auto i = std::make_unique<int>(2);

    auto l1 = [i=std::move(i)]() { return *i; };
    auto l2 = [](){ return 2; };

    using L1 = decltype(l1);
    using L2 = decltype(l2);

    if (true)
    {
        return Lambda_fixed_variant<L1, L2>{std::move(l1)};
    }
    return Lambda_fixed_variant<L1, L2>{l2};
}
Related