Trailing Return Types
/ 4 min read
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 syntaxstd::vector<int> get_numbers(int count);
// Trailing return type syntaxauto 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 buriedstd::unordered_map<std::string, std::vector<std::shared_ptr<User>>>get_user_groups(const DatabaseConnection& db, const std::string& query);
// Much cleanerauto 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:
- 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 ittemplate<typename Container>auto get_first_element(Container& c) -> decltype(c.front()) { return c.front(); // Preserves reference type}- 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&)>;};- 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; };}- SFINAE techniques:
// Enable function only if T has a size() methodtemplate<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 pointLambdas and Trailing Returns
Lambdas can use trailing return types too, which is sometimes necessary:
auto lambda1 = [](int x) { return x * 2; }; // Return type deducedauto lambda2 = [](int x) -> double { return x * 2.5; }; // Explicit return type
// Necessary when return type can't be deduced unambiguouslyauto 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;}
// Usagestd::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 returntemplate<typename T, typename U>auto add(T a, U b) -> decltype(a + b) { return a + b;}
// C++14 - auto deduction workstemplate<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 - confusingvoid (*old_style)(int, const std::string&);
// Function pointer with trailing return - clearerusing NewStyle = auto(int, const std::string&) -> void;
// Even clearer with std::functionusing 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 candidatesauto 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 casesint add(int a, int b); // Just use traditional syntaxTrailing return types are about readability - use them when they make the code clearer, not just because they’re “modern”.