Type Deduction for Functions

Consider the following program:

int multiply(int a, int b)
{
    return a * b;
}

When this function compiles, the compiler determines that a * b evaluates to an int, then ensures the return value's type matches the function's declared return type (or that the return value type can be converted to the declared return type).

Return Type Deduction With auto

Since the compiler already deduces the return type from the return statement (to ensure the value can be converted to the function's declared return type), in C++14, the auto keyword was extended to do function return type deduction. This works by using the auto keyword in place of the function's return type.

Example:

auto multiply(int a, int b)
{
    return a * b;
}

Because the return statement is returning an int value, the compiler deduces this function's return type is int.

When using an auto return type, all return statements within the function must return values of the same type, otherwise an error results. Example:

auto someFunction(bool flag)
{
    if (flag)
        return 100; // return type int
    else
        return 200.5; // return type double
}

In the above function, the two return statements return different types, so the compiler gives an error.

If such a case is desired for some reason, you can either explicitly specify a return type for your function (in which case the compiler will try to implicitly convert any non-matching return expressions to the explicit return type), or you can explicitly convert all your return statements to the same type. In the example above, the latter could be done by changing 100 to 100.0, but static_cast can also be used for non-literal types.

Benefits of Return Type Deduction

The biggest advantage of return type deduction is that having the compiler deduce the function's return type negates the risk of a mismatched return type (preventing unexpected conversions).

This can be particularly useful when a function's return type is fragile (cases where return type is likely to change if the implementation changes). In such cases, being explicit about the return type means having to update all relevant return types when an impacting change is made to the implementation. If we're lucky, the compiler will error until we update the relevant return types. If we're not lucky, we'll get implicit conversions where we don't desire them.

In other cases, a function's return type may either be long and complex, or not be that obvious. In such cases, auto can simplify:

// let compiler determine the return type of unsigned short + char
auto combine(unsigned short p, char q)
{
    return p + q;
}

We discuss this case more (and how to express such a function's actual return type) in a later lesson on function templates with multiple template types.

Downsides of Return Type Deduction

There are two major downsides to return type deduction:

  1. Functions using an auto return type must be fully defined before they can be used (a forward declaration is not sufficient). Example:
#include <iostream>

auto calculate();

int main()
{
    std::cout << calculate() << '\n'; // compiler has only seen a forward declaration at this point

    return 0;
}

auto calculate()
{
    return 50;
}

When tested, this gives the following compile error:

error C3779: 'calculate': a function that returns 'auto' cannot be used before it is defined.

This makes sense: a forward declaration doesn't have enough information for the compiler to deduce the function's return type. This means normal functions returning auto are typically only callable from within the file where they're defined.

  1. When using type deduction with objects, the initializer is always present as part of the same statement, so it's usually not overly burdensome to determine what type will be deduced. With type deduction for functions, that's not the case -- the function's prototype gives no indication as to what type the function actually returns. A good programming IDE should make clear what the deduced function type is, but in absence of that, a user would actually have to dig into the function body itself to determine what type the function returned. The odds of mistakes being made are higher. Generally we prefer to be explicit about types that are part of an interface (a function's declaration is an interface).

Unlike type deduction for objects, there isn't as much consensus on best practices for function return type deduction. We recommend generally avoiding return type deduction for functions.

Best practice: Prefer explicit return types over return type deduction (except in cases where the return type is unimportant, difficult to express, or fragile).

Trailing Return Type Syntax

The auto keyword can also declare functions using a trailing return syntax, where the return type is specified after the rest of the function prototype.

Consider this function:

int multiply(int a, int b)
{
  return (a * b);
}

Using trailing return syntax, this could be equivalently written as:

auto multiply(int a, int b) -> int
{
  return (a * b);
}

In this case, auto doesn't perform type deduction -- it's just part of the syntax to use a trailing return type.

Why would you want to use this? Here are some reasons:

  1. For functions with complex return types, a trailing return type can make the function easier to read:
#include <type_traits>

std::common_type_t<int, double> process(int, double); // harder to read (where is the function name in this mess?)
auto process(int, double) -> std::common_type_t<int, double>; // easier to read (we don't have to read the return type unless we care)
  1. The trailing return type syntax can align function names, making consecutive function declarations easier to read:
auto multiply(int a, int b) -> int;
auto divide(double a, double b) -> double;
auto displayResult() -> void;
auto extractSubstring(const std::string &text, int start, int length) -> std::string;

For advanced readers:

  1. If we have a function whose return type must be deduced based on function parameter types, a normal return type won't suffice, because the compiler hasn't yet seen the parameters at that point.
#include <type_traits>
// note: decltype(a) evaluates to the type of a

std::common_type_t<decltype(a), decltype(b)> multiply(int a, double b); // compile error: compiler hasn't seen definitions of a and b yet
auto multiply(int a, double b) -> std::common_type_t<decltype(a), decltype(b)>; // OK
  1. The trailing return syntax is also required for some advanced C++ features, such as lambdas (covered in a future lesson on lambdas).

For now, we recommend continued use of the traditional function return syntax except in situations requiring the trailing return syntax.

Type Deduction Can't Be Used for Function Parameter Types

Many new programmers who learn about type deduction try something like this:

#include <iostream>

void multiplyAndDisplay(auto a, auto b)
{
    std::cout << a * b << '\n';
}

int main()
{
    multiplyAndDisplay(5, 8); // case 1: call multiplyAndDisplay with int parameters
    multiplyAndDisplay(3.5, 2.25); // case 2: call multiplyAndDisplay with double parameters

    return 0;
}

Unfortunately, type deduction doesn't work for function parameters, and prior to C++20, the above program won't compile (you'll get an error about function parameters not being able to have an auto type).

In C++20, the auto keyword was extended so that the above program will compile and function correctly -- however, auto is not invoking type deduction in this case. Rather, it's triggering a different feature called function templates designed to actually handle such cases.

We introduce function templates in a later lesson on function templates, and discuss use of auto in the context of function templates in a lesson on function templates with multiple template types.

Summary

Return type deduction: C++14 feature allowing auto as a function's return type, letting the compiler deduce the return type from return statements.

Requirements: All return statements in a function with auto return type must return the same type. Functions must be fully defined (not just forward-declared) before use.

Benefits:

  • Eliminates risk of mismatched return types and unexpected conversions
  • Useful when return type is fragile, complex, or non-obvious
  • Simplifies code when exact return type depends on template parameters

Downsides:

  • Requires full definition before use (forward declarations insufficient)
  • Makes function interfaces less clear - users must examine implementation to know return type
  • Can make code harder to understand and maintain

Trailing return syntax: Alternative syntax auto functionName(params) -> ReturnType that places return type after parameters. Useful for complex return types, aligning function names, or when return type depends on parameter types.

Function parameters: Type deduction doesn't work for function parameters in C++17 and earlier. In C++20, auto parameters trigger function templates, not type deduction.

Best practice: Prefer explicit return types over return type deduction for most functions. Use auto return types only when the return type is unimportant, difficult to express, or fragile (likely to change with implementation changes).

Understanding function return type deduction helps you use it appropriately in situations where it genuinely improves code quality, while avoiding it where explicit types better serve code clarity and maintainability.