skip to content
Mehdi Mehdikhani
Table of Contents

Trailing return types in C++11 provide an alternative syntax for function declarations. They’re especially useful when the return type depends on the parameters or is just really complex.

What Trailing Return Types Actually Are

Instead of putting the return type at the beginning, you put it at the end with auto:

// Traditional syntax
std::vector<int> get_numbers(int count);
// Trailing return type syntax
auto get_numbers(int count) -> std::vector<int>;

Both declare the exact same function, just with different syntax.

Why This Is Useful

For simple functions, trailing returns don’t add much. But they shine when the return type is complex or depends on parameters:

// Hard to read - return type is buried
std::unordered_map<std::string, std::vector<std::shared_ptr<User>>>
get_user_groups(const DatabaseConnection& db, const std::string& query);
// Much cleaner
auto get_user_groups(const DatabaseConnection& db, const std::string& query)
-> std::unordered_map<std::string, std::vector<std::shared_ptr<User>>>;

The function name comes first, which is what you usually care about most.

When I Actually Use Trailing Returns

Most of the time, I use trailing return types for:

  1. Template functions where return type depends on parameters:
template<typename T, typename U>
auto multiply(T a, U b) -> decltype(a * b) {
return a * b; // Return type is whatever a * b produces
}
// C++14 made this simpler with auto deduction, but sometimes you still need it
template<typename Container>
auto get_first_element(Container& c) -> decltype(c.front()) {
return c.front(); // Preserves reference type
}
  1. Member function pointers and complex function types:
class EventHandler {
public:
using Callback = std::function<void(const Event&)>;
// Traditional way - hard to parse
std::function<void(const Event&)> (EventHandler::*get_callback())();
// Trailing return - much clearer
auto get_callback() -> std::function<void(const Event&)>;
};
  1. Functions returning lambdas or complex types:
auto create_validator(int min_value, int max_value)
-> std::function<bool(int)> {
return [min_value, max_value](int value) {
return value >= min_value && value <= max_value;
};
}
auto make_counter() -> std::function<int()> {
return [count = 0]() mutable { return ++count; };
}
  1. SFINAE techniques:
// Enable function only if T has a size() method
template<typename T>
auto get_size(const T& container)
-> decltype(container.size()) {
return container.size();
}
// This won't compile for types without size() - that's the point

Lambdas and Trailing Returns

Lambdas can use trailing return types too, which is sometimes necessary:

auto lambda1 = [](int x) { return x * 2; }; // Return type deduced
auto lambda2 = [](int x) -> double { return x * 2.5; }; // Explicit return type
// Necessary when return type can't be deduced unambiguously
auto conditional_lambda = [](bool flag) -> std::variant<int, std::string> {
if (flag) {
return 42;
} else {
return std::string("hello");
}
};

Real World Example

Here’s how I use trailing returns in a data processing pipeline:

template<typename InputIter, typename Predicate>
auto filter_and_transform(InputIter first, InputIter last, Predicate pred)
-> std::vector<decltype(pred(*first))> {
using ResultType = decltype(pred(*first));
std::vector<ResultType> result;
for (auto it = first; it != last; ++it) {
if (auto transformed = pred(*it); /* condition based on transformed */) {
result.push_back(transformed);
}
}
return result;
}
// Usage
std::vector<int> numbers{1, 2, 3, 4, 5};
auto squared_evens = filter_and_transform(
numbers.begin(),
numbers.end(),
[](int x) { return x % 2 == 0 ? x * x : 0; }
);

C++14 Simplification

C++14’s auto return type deduction eliminated many needs for trailing returns:

// C++11 - needed trailing return
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
// C++14 - auto deduction works
template<typename T, typename U>
auto add(T a, U b) {
return a + b; // Return type deduced from return statement
}

But trailing returns are still useful when you need precise control.

Function Pointer Syntax

Trailing returns make function pointer syntax much more readable:

// Traditional function pointer - confusing
void (*old_style)(int, const std::string&);
// Function pointer with trailing return - clearer
using NewStyle = auto(int, const std::string&) -> void;
// Even clearer with std::function
using Handler = std::function<void(int, const std::string&)>;

The Pattern I Follow

I use trailing return types when:

  • The return type is very long or complex
  • The return type depends on template parameters
  • I’m doing SFINAE or other template metaprogramming
  • The function name gets lost in a complex return type
// Good candidates
auto create_connection(const Config& cfg) -> std::unique_ptr<DatabaseConnection>;
template<typename T> auto process(T&& t) -> decltype(transform(std::forward<T>(t)));
// Not worth it for simple cases
int add(int a, int b); // Just use traditional syntax

Trailing return types are about readability - use them when they make the code clearer, not just because they’re “modern”.