Search code examples
c++lambdac++14language-lawyertrailing-return-type

What is the usage of lambda trailing return type auto?


What is the usage of adding -> auto in []() -> auto { return 4; }?

For me - it is not different than []() { return 4; }


Solution

  • It is auto by default. The Standard, [expr.prim.lambda]/4, reads:

    If a lambda-expression does not include a lambda-declarator, it is as if the lambda-declarator were (). The lambda return type is auto, which is replaced by the trailing-return-type if provided and/or deduced from return statements as described in [dcl.spec.auto].

    My addition.

    So, -> auto itself is not useful. However, we can form other return types with auto, namely: -> auto&, -> const auto&, -> auto&&, -> decltype(auto). Standard rules of return type deduction apply. One should bear in mind that auto is never deduced to be a reference type, so by default a lambda returns a non-reference type.

    A few (trivial) examples:

    // 1.
    int foo(int);
    int& foo(char);
    
    int x;
    
    auto lambda1 = [](auto& x) { return x; };
    static_assert(std::is_same_v<decltype(lambda1(x)), int>);
    
    auto lambda2 = [](auto& x) -> auto& { return x; };
    static_assert(std::is_same_v<decltype(lambda2(x)), int&>);
    
    // 2.
    auto lambda3 = [](auto x) { return foo(x); };
    static_assert(std::is_same_v<decltype(lambda3(1)), int>);
    static_assert(std::is_same_v<decltype(lambda3('a')), int>);
    
    auto lambda4 = [](auto x) -> decltype(auto) { return foo(x); };
    static_assert(std::is_same_v<decltype(lambda4(1)), int>);
    static_assert(std::is_same_v<decltype(lambda4('a')), int&>);
    
    // 3.
    auto lambda5 = [](auto&& x) -> auto&& { return std::forward<decltype(x)>(x); };
    static_assert(std::is_same_v<decltype(lambda5(x)), int&>);
    static_assert(std::is_same_v<decltype(lambda5(foo(1))), int&&>);
    static_assert(std::is_same_v<decltype(lambda5(foo('a'))), int&>);
    

    PiotrNycz's addition. As pointed out in comments (credit to @StoryTeller) - the real usage is version with auto& and const auto& and "The degenerate case is just not something worth bending backwards to disallow."

    See:

    int p = 7;
    auto p_cr = [&]()  -> const auto& { return p; };
    auto p_r = [&]()  -> auto& { return p; };
    auto p_v = [&]()  { return p; }; 
    
    const auto& p_cr1 = p_v(); // const ref to copy of p
    const auto& p_cr2 = p_cr(); // const ref to p
    p_r() = 9; // we change p here
    
    std::cout << p_cr1 << "!=" << p_cr2 << "!\n";
    // print 7 != 9 !