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 an 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 ;
where category can be either
auto
or using
. Examples:
auto hello = "Hello"s; auto f(double) -> int; using dict = std::map<std::string, std::string>;
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.
Great summary, thanks for the writeup!
>> Since the compiler parses the source code from left to right, it sees lhs and rhs
but how should work this code in C++14
or in C++14 compiler doesn’t parse code from left to right?
Basically, the compiler sees
auto f(
and notes that forf
, either a trailing return type will be specified later, or the return type will have to be deduced. As soon as it hits{
, it sees that there is no trailing return type (there would have been-> Type
otherwise). So, it notes that it will need to deduce the return type automatically later once the function’s body is parsed.Automatic type deduction is a C++14 feature. In C++11, this would be an error.
So is this article from 2017, specifically covering C++11?
I mean, with C++14, why I would use this strange alternative syntax instead of:
Hi Lukáš, the article covers C++14 as well, and there is actually a mention of C++14 at the beginning of the article, including a statement that this feature of C++14 is out of scope of the article. You are correct that if you do not need or want to specify an explicit return type, you can use just
auto
. The article is focused on cases when you either want or need to specify the return type explicitly, even if this might not be apparent from the text.(BTW, I am sorry about the issues with formatting/preview. I took the liberty of fixing the formatting in your original comment.)
It’s not a problem here, since
unambiguously refers to a function definition with no trailing return type; the compiler knows what it’s parsing. At that point, it can store a “context” about what it’s parsing, and only deduce the return type later. It might not have been possible with older compilers, which might not have had enough memory to store such a big context.
The reason why
does not work in the above example is because as soon as the compiler reads “lhs”, it doesn’t know what the name means (is it a type? a function? a global variable?), and therefore as to stop trying to understand. It cannot just store a context about the name, because it does not understand in what context it even is.
i think compiler works with syntax tree. before compilation functions looks like
there is no left and right, before and after.
at least ‘decltype’ says what result of this expression will be type, and exact type compiler can find out after it will see whole function, like now it is working with ‘auto’.
i think, there have to be better explanation, why they did this in this way.
I am not sure if I understand what you are asking. Could you please try to elaborate on your question, like trying to rephrase it or add more details?
Omission look like an addition to the old syntax instead of a “feature” of the new one
Well, that depends on whether you consider
auto func() { ... }
to be in the old or in the new syntax :). I guess you can look at it in both ways. For example, in C++14, you can also write e.g.auto& func() { ... }
.For programmers coming from or having to work with Swift the new syntax would actually be the consistent and familiar one :)
Swift either borrowed its function syntax from C++11 or they both borrowed from the same source.
Yeah :). Rust also uses this syntax, and I like it.
As for who borrowed it from whom, the syntax is based on mathematical notation for functions (e.g.
f: X -> Y
denotes a functionf
fromX
toY
). Many functional languages have been using it. So, I guess both C++ and Swift got their inspiration from there.Hi Petr,
Do you think it is worth noting that the Alternative Function Syntax makes C++ functions look like their mathematical counterparts?
Like
R -> R+
f:x |-> x²
I believe this has been adopted by several functional programming languages, like Haskell for example. Wouldn’t know whether to consider this a pro or a con though, would you have an opinion on this?
Great article btw and I read some of your other posts. I really appreciate your synthetic way of presenting things, this is great work. Keep it up!
Hi Jonathan! Thank you for your kind words.
Yes, it is definitely worth noting where this syntax came from. In general, I consider the “arrow syntax” to be a pro because it is used by many programming languages as well as in math. This makes its meaning apparent, even for people who have just started using such a language. Even Python’s type hinting uses this syntax (e.g.
def name() -> str: return 'Joe'
).Generally, I like the “arrow syntax” very much. However, I can’t shake the feeling that for C++ the new syntax looks kind of weird :). I have been programming in C++ for nearly 10 years, so maybe I am just too much used to the original syntax.
A remark about automatic return type deduction. It can not be used with non template functions you want to share between multiple translation unit.
Example :
Putting f body inside f.h will solve that issue but bring linker error
Yes. The deduced type depends on the body of the function. Without a body, the compiler has not way of deducing the return type.
As for the linker error, if you move the function’s body into the header file, you need to mark it
inline
to allow multiple definitions of the function (the header may be included in multiple translation units). Then, you should also remove the definition inf.cpp
as the function definition is now inf.h
.I heavily use this syntax in my open source project mainly to just try it out. If anyone is interested in what a large codebase would look like if you use this throughout; have a look here:
https://github.com/aeon-engine/aeon-engine/tree/master/engine
Good information about the C++ language. Thank you for sharing. I will read your upcoming article.
I would like to know the following:
If I write pointer to a function taking an int as a parameter and returning int as follows:
Well, then how can I write the array of such pointers of 10 elements.?
Be advised that such approach as
does not work.
An array of 10 pointers to functions taking an int and returning an int can be written as follows:
What compiler do you use? The above code compiles just fine with both GCC and Clang.
I use MS Visual Studio 2017
I had been already answered that this style of writing will be realized in future versions of MS Visual Studio.
Nice explanation.
I think there is typo.
“The correct answer is a function taking and int and …”
Instead
“The correct answer is a function taking an int and …”
Thank you for the report. Fixed.