What Is the Meaning of Auto When Using C++ Trailing Return Type

What is the meaning of auto when using C++ trailing return type?

In general, the new keyword auto in C++11 indicates that the type of the expression (in this case the return type of a function) should be inferred from the result of the expression, in this case, what occurs after the ->.

Without it, the function would have no type (thus not being a function), and the compiler would end up confused.

Advantage of using trailing return type in C++11 functions

In this example, they mean the exact same thing.

However, there are a few advantages to using the trailing return type form consistently (Phil Nash calls these "East End Functions", since the return type is on the east end).

  1. Using parameters. Obviously when using parameters to determine the return type, you must use a trailing return type.

     template <typename T>
    auto print(T const& t) -> decltype(std::cout << t) { return std::cout << t; }
  2. Name lookup. In a trailing return type, name lookup includes the class scope for member function definitions. This means you don't have to retype the class if you want to return a nested class:

     Type C::foo() { ... }         // error: don't know what Type is
    C::Type C::foo() { ... } // ok

    auto C::foo() -> Type { ... } // ok
  3. Likewise, for defining member functions where the class name for some reason must be disambiguated to be in the global namespace and the return type is a class:

     D ::C::foo() { ... }         // error, parsed as D::C::foo() { ... }

    auto ::C::foo() -> D { ... } // ok
  4. A more reasonable ordering of information. Let's say you want to write a function to_string that takes an int and returns an string. That's a pretty sensible way of phrasing that. Function name, parameters, return type. You wouldn't say you want to write a string-returning function named to_string that takes an int. That's an awkward order of information. The name is the most important, followed by the parameters, followed by the return type. The trailing-return-type form allows you to order these pieces of information better.

There are cases where trailing-return-type is mandatory, there are cases where it is helpful, and there are cases where it does the same thing. There are not cases where it is worse for reasons other than simply character count.

Plus, mathemtically we're used to thinking of functions as A -> B and not so much B(A), and so auto(*)(A) -> B as a function pointer taking an A and returning a B is a little closer to that view than B(*)(A).


On the other hand, writing auto main() -> int looks ridiculous.

But honestly, that's mostly because of unfamiliarity. There's nothing inherently ridiculous about it. If anything, it's a bit unfortunate that the language uses auto here as a way to declare a function - not because it's too long (i.e. some other languages use fun or even fn) - but because it's not really distinct from other uses of auto. If it were func instead, I think it would have been better (although it would not make any sense to change now).


Ultimately, this is purely opinion based. Just write code that works.

auto, decltype(auto) and trailing return type

Trailing return type should only be used with auto

The point of decltype(auto) vs auto is to distinguish the case whether the return type should be a reference or value. But in your case the return type is already explicitly defined as decltype(std::get<0>(std::forward<T>(x))), so it will be perfectly-forwarded even if you use auto.

In auto f() -> T, the "auto" keyword is simply a syntactic construct to fill in a type position. It serves no other purpose.


In fact, in C++17 you cannot use a decltype(auto) with trailing-return-type together.

C++14 wordings (n3936 §7.1.6.4[dcl.spec.auto]/1):

The auto and decltype(auto) type-specifiers designate a placeholder type that will be replaced later, either by deduction from an initializer or by explicit specification with a trailing-return-type. The auto type-specifier is also used to signify that a lambda is a generic lambda.

C++17 wordings (n4618 §7.1.7.4[dcl.spec.auto]/1):

The auto and decltype(auto) type-specifiers are used to designate a placeholder type that will be replaced later by deduction from an initializer. The auto type-specifier is also used to introduce a function type having a trailing-return-type or to signify that a lambda is a generic lambda (5.1.5). The auto type-specifier is also used to introduce a decomposition declaration (8.5).

This is DR 1852, see Does a placeholder in a trailing-return-type override an initial placeholder?.

Practically, while gcc accepts decltype(auto) f() -> T (which is a bug), but clang will reject it saying

error: function with trailing return type must specify return type 'auto',
not 'decltype(auto)'

Can i use auto or decltype instead trailing return type?

auto& get_diag2(int(&ar)[3][3]){ // adding & auto because otherwise it converts the array to pointer
static int diag[3]{
ar[0][0], ar[1][1], ar[2][2]
};
return diag;
}

Will not work in a C++11 compiler. Using auto without a trailing return type was added to C++14 and acts like how auto works when using it for a variable. This means it will never return a reference type so you have to use auto& to return a reference to the thing you want to return.

If you do not know if you should return a reference or a value (this happens a lot in generic programming) then you can use decltyp(auto) as the return type. For example

template<class F, class... Args>
decltype(auto) Example(F func, Args&&... args)
{
return func(std::forward<Args>(args)...);
}

will return by value if func returns by value and return by reference if func returns a reference.


In short if you are using C++11 you have to specify the return type, either in front or as a trailing return type. In C++14 and above you can just use auto/decltype(auto) and let the compiler deal with it for you.

What is the usage of lambda trailing return type auto?

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 !

Trailing return type in C++14

Consider...

auto f(int x)
{
if (x == 2)
return 3;
return 2.1;
}

...this has an ambiguous return type - int or double. An explicit return type - whether prefixed or trailing - can disambiguate it and casts the return argument to the return type.

Trailing return types specifically are also useful if you want to use decltype, sizeof etc on some arguments:

auto f(int x) -> decltype(g(x))
{
if (x == 2)
return g(x);
return 2;
}

Does decltype(auto) make trailing return type obsolete?

decltype(auto) (and more generally deduced return type) and trailing return type are orthogonal features.

You can have:

  • decltype(auto) f() {}
  • auto f() -> decltype(auto) {}

Trailing return type

trailing return type is fine especially to have access to context we don't have before the function name

  • as for template:

    template <typename T>
    auto f(T x) -> decltype(bar(x));

    versus

    template <typename T>
    decltype(bar(std::declval<T&>())) f(T x);
  • or for dependent name in class:

    auto C::begin() -> iterator;

    versus

    C::iterator C::begin();

The only place where it is required is for lambda (if you have/want to specify return type explicitly):

  • []() -> some_type {/*...*/}
  • []() -> auto {/*...*/} (which is equivalent to []() {/*...*/})
  • []() -> decltype(auto) {/*...*/}

Case when we have to defining return type of lambda is when it should return reference type.

Deduced return type

Done with decltype(auto) and auto.

decltype(auto) and auto deduction type differs, mostly as T&& and T.

Deduced return type requires definition of the body.

They also doesn't allow SFINAE, as there are no substitution.



Related Topics



Leave a reply



Submit