Should the Trailing Return Type Syntax Style Become the Default For New C++11 Programs

Should the trailing return type syntax style become the default for new C++11 programs?

There are certain cases where you must use a trailing return type. Most notably, a lambda return type, if specified, must be specified via a trailing return type. Also, if your return type utilizes a decltype that requires that the argument names are in scope, a trailing return type must be used (however, one can usually use declval<T> to work around this latter issue).

The trailing return type does have some other minor advantages. For example, consider a non-inline member function definition using the traditional function syntax:

struct my_awesome_type
{
typedef std::vector<int> integer_sequence;

integer_sequence get_integers() const;
};

my_awesome_type::integer_sequence my_awesome_type::get_integers() const
{
// ...
}

Member typedefs are not in scope until after the name of the class appears before ::get_integers, so we have to repeat the class qualification twice. If we use a trailing return type, we don't need to repeat the name of the type:

auto my_awesome_type::get_integers() const -> integer_sequence
{
// ...
}

In this example, it's not such a big deal, but if you have long class names or member functions of class templates that are not defined inline, then it can make a big difference in readability.

In his "Fresh Paint" session at C++Now 2012, Alisdair Meredith pointed out that if you use trailing return types consistently, the names of all of your functions line up neatly:

auto foo() -> int;
auto bar() -> really_long_typedef_name;

I've used trailing return types everywhere in CxxReflect, so if you're looking for an example of how code looks using them consistently, you can take a look there (e.g, the type class).

Reason for decltype in trailing return type

Note

Without it, the compiler would not be able to derive the return type of the template function.

This was later fixed in C++14 and the compiler doesn't need the trailing return type as it can infer the return type from the returned expression. The following works fine in C++14.

template <typename lhsT, typename rhsT>
auto add(const lhsT& lhs, const rhsT& rhs) {
return lhs + rhs;
}

But why is the syntax the way it is?

The use of trailing return type is useful when the return type is deduced from the arguments, which always are declared after the return type (one can't refer to something that hasn't been declared yet).

Also, a trailing return type does appear to be more natural in the sense that functional and mathematical notation uses this kind of declaration. I think most types of notation outside of C-style declarations commonly use trailing return type. E.g.:

f : X → Y

In above mathematical notation, f is the function name, X the argument, and Y the returned value.

Just because the flock of pink sheep reject the gray sheep doesn't make it unnatural.

In your second example it is not possible for the compiler to infer the return types as of above reason, i.e., that the arguments have not yet been declared. However, inference is possible if using the template parameters and std::declval, e.g.:

template <typename T, typename U>
decltype(std::declval<T>() + std::declval<U>()) add(const T& lhs, const U& rhs) {
return lhs + rhs;
}

std::declval acts as a lazy instantiation of a type and never evaluates, e.g., calls the constructor etc. It is mostly used when inferring types.

Should main with trailing return type be avoided?

It's perfectly valid and works just fine.

The only issue to concern is that it is new. It may confuse or surprise readers of your code who are only familiar with C++98.

But it works, so feel free to write your main this way if you feel like it.

Trailing return type in non-template functions

In addition to sergeyrar's answer, those are the points I could imagine someone might like about trailing return types for non-template functions:

  1. Specifying a return type for lambda expressions and functions is identical w.r.t. the syntax.

  2. When you read a function signature left-to-right, the function parameters come first. This might make more sense, as any function will need the parameters to produce any outcome in the form of a return value.

  3. You can use function parameter types when specifying the return type (the inverse doesn't work):

    auto f(int x) -> decltype(x)
    {
    return x + 42;
    }

    That doesn't make sense in the example above, but think about long iterator type names.

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.

Why is the trailing return type necessary in this lambda expression?

If you explicitly give the return type and std::is_invocable_v<decltype(f), decltype(arg), decltype(args)...> is false SFINAE applies and a test for whether or not the lambda is callable will simply result in false.

However without explicit return type, the return type will need to be deduced (in this case because of std::is_invocable_v<decltype(f)> being applied to the lambda) and in order to deduce the return type the body of the lambda needs to be instantiated.

However, this also instantiates the expression

return std::invoke(f, arg, args...);

which is ill-formed if std::is_invocable_v<decltype(f), decltype(arg), decltype(args)...> is false. Since this substitution error appears in the definition rather than the declaration, it is not in the immediate context and therefore a hard error.

Is there an intention behind the auto keyword in trailing return type function syntax?

The paper "Decltype (revision 5)", N1978 proposed the syntax for trailing-return-type (as they're now known). This was done to simplify defining function templates whose return type depends on an expression involving its arguments in chapter 3:

template <class T, class U> decltype((*(T*)0)+(*(U*)0)) add(T t, U u);

The expression (*(T*)0) is a hackish way to write an expression that has the type T and does not require T to be default
constructible. If the argument names were in scope, the above
declaration could be written as:

template <class T, class U> decltype(t+u) add(T t, U u);

Several syntaxes that move the return type expression after the
argument list are discussed in [Str02]. If the return type expression
comes before the argument list, parsing becomes difficult and name
lookup may be less intuitive; the argument names may have other uses
in an outer scope at the site of the function declaration.

We suggest reusing the auto keyword to express that the return type
is to follow after the argument list. The return type expression is
preceded by -> symbol, and comes after the argument list and
potential cv-qualifiers in member functions and the exception
specification:

template <class T, class U> auto add(T t, U u) -> decltype(t + u);

The reference [Str02] is "Bjarne Stroustrup. Draft proposal for "typeof". C++ reflector message c++std-ext-5364, October 2002.", but I'm not sure if that's publicly available.

Why is return type before the function name?

As always, K&R are the "bad guys" here. They devised that function syntax for C, and C++ basically inherited it as-is.

Wild guessing here:
In C, the declaration should hint at the usage, i.e., how to get the value out of something. This is reflected in:

  • simple values: int i;, int is accessed by writing i
  • pointers: int *p;, int is accessed by writing *p
  • arrays: int a[n];, int is accessed by writing a[n]
  • functions: int f();, int is accessed by writing f()

So, the whole choice depended on the "simple values" case. And as @JerryCoffin already noted, the reason we got type name instead of name : type is probably buried in the ancient history of programming languages. I guess K&R took type name as it's easier to put the emphasis on usage and still have pointers etc. be types.

If they had chosen name : type, they would've either disjoined usage from declarations: p : int* or would've made pointers not be types anymore and instead be something like a decoration to the name: *p : int.

On a personal note: Imagine if they had chosen the latter and C++ inherited that - it simply wouldn't have worked, since C++ puts the emphasis on types instead of usage. This is also the reason why int* p is said to be the "C++ way" and int *p to be the "C way".

If so, is there a reason for a hypothetical new programming language to not use trailing return type by default?

Is there a reason to not use deduction by default? ;) See, e.g. Python or Haskell (or any functional language for that matter, IIRC) - no return types explicitly specified. There's also a movement to add this feature to C++, so sometime in the future you might see just auto f(auto x){ return x + 42; } or even []f(x){ return x + 42; }.

what's `auto classMemberFunction()->void {}` signature?

This is an (ab)use of the trailing-return-type syntax that has been introduced in C++11. The syntax is:

auto functionName(params) -> returnType;
auto functionName(params) -> returnType { }

It works the same as a classic function declaration with the return type on the left, except that the trailing type can use names introduced by the function's signature, i.e:

T    Class::function(param);      // No particular behaviour
auto Class::function(param) -> T; // T can use Class::Foo as Foo, decltype(param), etc.

In this case though, there is no point except consistency.



Related Topics



Leave a reply



Submit