Pros and Cons of Alternative Function Syntax in C++

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

Lets 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.

Lets 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 ;

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, lets 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.

14 Comments

  1. >> 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

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

    or in C++14 compiler doesn’t parse code from left to right?

    Reply
    • Basically, the compiler sees auto f( and notes that for f, 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.

      Reply
    • It’s not a problem here, since

      auto name(type name) {

      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

      decltype(lhs + rhs)

      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.

      Reply
      • i think compiler works with syntax tree. before compilation functions looks like

        functionNode (name)
            expressionNode (returnValue)
            argumentsList (arguments)
               argument(name, type)
               ...
            statementLits(body)
               statement(...)
               ....
        

        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.

        Reply
    • 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() { ... }.

      Reply
  2. 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.

    Reply
  3. 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!

    Reply
    • 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.

      Reply
  4. 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 :

    // f.h
    auto f();
    
    // f.cpp 
    auto f(){return 0;}
    
    // main.cpp
    #include "f.h"
    // the compiler complains that it cannot deduce f return type
    f()
    

    Putting f body inside f.h will solve that issue but bring linker error

    Reply
    • 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 in f.cpp as the function definition is now in f.h.

      Reply

Leave a Comment.