Skip to content

C++ Trailing Return Types

The other day, I was modernizing the PMP library code using clang-tidy. One of the suggestions was to use trailing function return types. I didn’t look too close at this C++11 feature before. Read on for a brief introduction and a summary of pros and cons.

What’s This?

Trailing return types are an alternative syntax introduced in C++11 to declare the return type of a function. In the old form you specify the return type before the name of the function:

int max(int a, int b);

With the new syntax you write auto before the function name and specify the return type after the function name and parameters:

auto max(int a, int b) -> int;

This looks strange to a C++ developer at first. However, other languages like Swift, Rust, or Haskell use a similar notation. Thinking of auto as a func keyword might help.

Background

The main motivation for this alternative syntax is to specify the return type of a function template when the return type depends on the template arguments. Consider the following function:

template<typename A, typename B>
??? multiply(A a, B b) { return a*b; }

In this case, the general return type of multiply() is decltype(a*b). However, you can’t write this using the standard function syntax. C++ parses from left to right. Therefore, the function parameters a and b are not yet in scope when specifying the return type before the function name.

There is a workaround using declval(), but this gets confusing:

template<typename A, typename B>
decltype(std::declval<A&>() * std::declval<B&>())
multiply(A a, B b) { return a*b; }

The alternative syntax solves this problem. Now you can directly write the correct return type:

template<typename A, typename B>
auto multiply(A a, B b) -> decltype(a*b) { return a*b; }

Pros

With two different ways to specify the return type the question comes up which one to use. Let’s have a look at the pros first.

Consistency

I think consistency is a strong argument for the new syntax. The example above shows that the new syntax is helpful in certain cases. Lambda functions use trailing return types as well:

auto square = [] (int n) -> int { return n*n; }

The new syntax is aligned with the general trend of modern C++ to move towards a left to right syntax:

auto message = std::string{"Hello"};
using table = std::map<std::string, int>;
auto square = [] (int n) -> int { return n*n; }

Herb Sutter goes into more detail in his Almost Always Auto Style article.

Understanding

When reading a function declaration the main thing I’m interested in is understanding what it does. Assuming that functions are properly named by their developers, the most important information is in the function name and not the return type. Therefore, it makes sense to put the name first.

A trailing return type is also closer to mathematical notation and the way we speak about functions in math. Let $f\;\colon\;\mathbb{R} \to \mathbb{R}$ be a function…

Readability

Using trailing return types neatly aligns the function names:

auto is_empty() -> bool;
auto number_of_vertices()() -> int;
auto vertices_begin() -> VertexIterator;

The notation can be shorter than the standard return type, for example when returning a type defined inside a class such as VertexIterator above. The standard syntax would require the class scope to be specified explicitly:

SurfaceMesh::VertexIterator SurfaceMesh::vertices_begin() { ... }

The new syntax is slightly more concise:

auto SurfcaceMesh::vertices_begin() -> VertexIterator { ... }

Cons

Two main drawbacks come to mind.

Lack of Familiarity

Obviously, most existing code does not use the new syntax. Therefore, not all developers are familiar with it. There’s also a pitfall with override, which has to go after the return type since override is not part of the function type:

virtual auto foo() -> int override;

I’ve seen the new syntax being used in some open source libraries. However, they are clearly in the minority. Using the new syntax therefore would make your library inconsistent with large parts of the C++ ecosystem.

More Typing

Using the new syntax can require more typing, notably for trivial functions. I don’t think that many folks would prefer writing

auto func() -> void;

instead of

void func();

Excluding such cases would kill the consistency argument from above.

Conclusion

I’m not sure if using trailing return types everywhere is a good idea. The consistency argument and potential improvements in readability and understanding almost got me sold. They’re only nice to have though, and not much more.

The lack of familiarity is a major reason not to use the new syntax. Furthermore, C++14 introduced the possibility to automatically deduce the return type. With that, the multiply() example from above gets even simpler:

template<typename A, typename B>
auto multiply(A a, B b) { return a*b; }

This reduces the number of cases where the new return syntax is required, thereby making it a rather odd choice for normal function declarations.

Keep in mind though that auto return type deduction is not always what you want. It’s certainly fine for short functions like the example above. In general, however, I prefer explicit return types so that users know what they get without reading and understanding the full implementation or relying on an IDE.

Further Reading

Join the discussion on Reddit. Thanks for all the comments. Special thanks to u/jwakely for spotting an inaccuracy with std::declval<> parameter types.