C++11 introduced an alternative syntax for writing function declarations. Instead of putting the return type before the name of the function (e.g. int func() ), the new syntax allows us to write it after the parameters (e.g. auto func() -> int ). This leads to a couple of questions: Why was such an alternative syntax added? Is it meant to be a replacement for the original syntax? To help you with these questions, the present blog post tries to summarize the advantages and disadvantages of this newly added syntax.



Introduction

Since C++11, we have a new way of declaring functions. This alternative function syntax allows us to write the following function

// C or C++98 int f(int x, int y) { // ... }

as

// C++11 auto f(int x, int y) -> int { // ... }

Basically, instead of writing the return type before the name of the function, we put there just auto and specify the return type after the parameter list. Since the return type appears at the end of the declaration, the function is said to have a trailing return type.

Both of the declarations above are equivalent, which means that they mean exactly the same. This leads to a couple of questions: Why was such an alternative syntax added? Is it meant to be a replacement for the original syntax? I will try to shed some light on these questions by trying to summarize the benefits and disadvantages.

As a side note, the use of the auto keyword here is only part of the syntax and does not perform automatic type deduction in this case. Automatic type deduction of functions was added in C++14, and would kick in if we did not provide the trailing return type:

// C++14 auto f(int x, int y) { // The return type is deduced automatically // based on the function's body. // ... }

Use of automatically deduced return types has its own pros and cons and will not be discussed in the present post.

Pros

Let’s start with the advantages.

Simplification of Generic Code

Consider the following function, written using the alternative syntax:

// C++11 template<typename Lhs, typename Rhs> auto add(const Lhs& lhs, const Rhs& rhs) -> decltype(lhs + rhs) { return lhs + rhs; }

It has two parameters and returns their sum. Note that the parameters may have different types, which is why we used two different template parameters. As long as the types support binary + , they can be used as arguments to add() . The decltype specifier gives us the type of the expression lhs + rhs .

Let’s try to rewrite the function using the standard syntax:

template<typename Lhs, typename Rhs> decltype(lhs + rhs) add(const Lhs& lhs, const Rhs& rhs) { // error: ^^^ 'lhs' and 'rhs' were not declared in this scope return lhs + rhs; }

Oops. Since the compiler parses the source code from left to right, it sees lhs and rhs before their definitions, and rejects the code. By using the trailing return type, we can circumvent this limitation.

A note for the interested reader: The above function can be written using the standard syntax with the help of declval() :

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

However, as you can see, it makes the code less readable.

Elimination of Repetition

Consider the following class:

class LongClassName { using IntVec = std::vector<int>; IntVec f(); };

To define f() using the standard syntax, we have to duplicate the class name:

LongClassName::IntVec LongClassName::f() { // ... }

The reason is similar to the one in the previous example: The compiler parses the code from left to right, so if it saw IntVec , it would not know where to look for it because the context ( LongClassName ) is given after the return type. With the new syntax, there is no need to repeat LongClassName :

auto LongClassName::f() -> IntVec { // ... }

May Lead To More Readable Code

A quick question: What does the following declaration declare?

void (*get_func_on(int i))(int);

The correct answer is a function taking and int and returning a pointer to a void function taking an int. A declaration of this function using the new syntax makes this obvious:

auto get_func_on(int i) -> void (*)(int);

Consistency

Last, but certainly not least, uniform use of the new syntax may lead to more consistent code. For example, when you define a lambda expression, its return type can specified only as the trailing return type:

[](int i) -> double { /* ... */ };

There is no “old” return-type syntax for lambda expressions, so you cannot write the return type at the left-hand side.

More generally, as pointed out by Herb Sutter, the C++ world is moving to a left-to-right declaration style everywhere, of the form





category name = type and/or initializer ;



auto

using

auto hello = "Hello"s; auto f(double) -> int; using dict = std::map<std::string, std::string>;

where category can be eitheror. Examples:

Finally, a somewhat nice property of the new syntax is that functions declarations are now neatly lined up by their name:

auto vectorize() -> std::vector<int>; auto devour(Value value) -> void; auto get_random_value() -> Value;

However, as pointed out here, the aligning of function names looks more readable only when the functions take one line each.

Cons

Alright. Now that we saw all the goodies, let’s take a look at the disadvantages.

Omission Can Cause a Copy To Be Returned

In C++14, if you forget to specify the trailing return type, a return type will be automatically deduced. Unfortunately, the deduced type may not be what you want. For example, consider the following standard definition of an assignment operator:

auto MyClass::operator=(const MyClass& other) -> MyClass& { value = other.value; return *this; }

If you omit the trailing return type, the code will compile, but it will return a value instead of a reference:

auto MyClass::operator=(const MyClass& other) { value = other.value; return *this; // Oops, returns a copy of MyClass! }

Indeed, automatic type deduction via auto never deduces a reference (if you want a reference, use auto& instead). A careless omission may thus silently change the semantics of your code.

Can Produce Longer Declarations

Sometimes the new syntax produces longer declarations:

int func(); // vs auto func() -> int;

Unexpected Position with Override

Your mileage may vary, but I have seen people bitten by this. Consider the following code:

struct A { virtual int foo() const noexcept; }; struct B: A { virtual int foo() const noexcept override; };

Some people expect the declaration of B::foo() with the new syntax to look like this:

virtual auto foo() const noexcept override -> int; // error: virtual function cannot have deduced return type

Oops. The correct form is

virtual auto foo() const noexcept -> int override;

That is, override has to be specified after the trailing return type (reason).

Consistency

If you recall the list of advantages, you may remember consistency to be one of the perks. However, this only applies to new code. Most of the existing code has been written using the standard syntax. Thus, when you start to use the new style, your coding style may actually become inconsistent.

Not a Widely Known Feature

In general, programmers are not familiar with the new syntax. While this is of course not a reason against the new syntax, it is something to keep in mind.

Weirdly Looking Syntax

Finally, for people that have been programming in C and C++ for a very long time, the new syntax looks weird. So, think twice before writing

auto main() -> int {}

as this may cause that your co-workers will want to hit you with a stick :).

Conclusion

The alternative syntax was added to aid writing of generic code and to provide consistency. However, due to the various disadvantages listed above, the original syntax is used more widely than the new syntax. Even C++ Core Guidelines generally use the original syntax.

Discussions

You can also discuss this post at /r/cpp.