4.5. Trailing return types

Problem:

  • You created a template, but the return type needs to be a type other than one of the template parameters.

You'd like to be able to use auto to simply write:

template<class T, class U>
auto mystery_function(T t, U u);

In modern C++, this syntax works well when the compiler can deduce one return type from the function body. For example:

constexpr auto square(int value) {
  return value * value;
}

The compiler sees that value * value produces an int, so the function return type is deduced as int. The same idea works for many simple function templates:

template <typename T, typename U>
constexpr auto add(const T& lhs, const U& rhs) {
  return lhs + rhs;
}

However, there are still cases where writing the return type explicitly is clearer or necessary.

Even in C++17, depending on what a function does, return type deduction may not always work. If it is possible for our function to return different types, then a simple auto return will not compile:

auto f(bool val)
{
    if (val) return 123; // deduces return type int
    else return 3.14f;   // error: deduces return type float
}

You can use auto, together with the decltype type specifier, to delay the evaluation of a function return value until after the function parameters have been declared.

Use auto and decltype to declare a function whose return type depends on the types of its arguments.

To understand what's going on, first we have to understand decltype.

4.5.1. Keyword: decltype

Added in C++11, the decltype type specifier yields the type of a specified expression, object, or literal value. We use decltype when we want to define a variable based on the result of an expression, but we don't want to use the expression to initialize the variable's value. For example:

int i = 42;
decltype(i) j = i * 2.0;

Similarly, there is a symmetry between the auto specifier and decltype:

auto a = 3u;       // a is unsigned;
decltype(a) b = a; // same as auto b = a;  b is also unsigned

4.5.2. Trailing return type syntax

Since the auto specifier and decltype are complementary operators, they work well together to help write generic functions that avoid committing to a specific type.

To declare a trailing return type for a function, use this general form:

auto function_name () -> return_type
{
  // function body
}

The -> is required to inform the compiler that a trailing return type follows.

Note that the return type is inserted after function parameters and before the function body.

constexpr auto f(const bool val) -> float
{
    if (val) return 123; // return widens int to float
    else return 3.14f;   // return type float
}
template<typename T, typename U>
constexpr auto add(const T& lhs, const U& rhs) -> decltype(lhs + rhs)
{
    return lhs + rhs;
}

Calling this add function like so:

auto val = std::numeric_limits<unsigned short>::max(); // typically 65,536
auto sum = add(val, val);

Even though a variable of type unsigned short was used in both parameters, the return type can't be unsigned short, because the returned value is too large to fit. If we had committed to a type, or used one of the generic types provided in the template, our result would overflow. Instead, the compiler used decltype to determine in this case, the return type should be int.

Do trailing return types seem like a lot of trouble? Without a trailing return type, you can use decltype and declval to describe the return type before the parameter names exist:

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

This gets unreadable fairly quickly. For this reason, trailing return types are preferred.


More to Explore