Trailing Return Type C++ 11
An alternative method of indicating a function's return type in C++ is to use trailing return types, which place the function's return type after the parameter list rather than before the function name. The main objective of this C++11 feature was to improve code readability, particularly in situations involving complex return types.
Syntax:
It has the following syntax:
auto function_name(parameter_list) -> return_type {
// function body
}
Benefits:
There are several benefits of the trailing return type in C++. Some benefits of the trailing return type in C++ are as follows:
Improved Readability: Function declarations are frequently made easier to understand by trailing return types, especially when the functions have complex return types.
Flexibility in Type Deduction: They allow type deduction to rely on parameters, which is necessary for some template-based methods.
Decltype Integration: Trailing return types seamlessly integrate with decltype to determine types based on expressions inside the function.
// Traditional return type declaration:
std::vector<int>::iterator find_element(std::vector<int>& vec, int value) {
// ...
}
// Trailing return type declaration:
auto find_element(std::vector<int>& vec, int value) -> std::vector<int>::iterator {
// ...
}
When to Consider Trailing Return Types:
- When the type of return is complex or long.
- When composing template functions that rely on template arguments for their return types.
- When the return type is automatically determined using decltype.
- When aiming for a coding style that is consistent and puts readability first.
Syntax of Trailing Return Type
Here's the basic syntax for using trailing return types in C++ function declarations:
auto function_name(parameter_list) -> return_type {
// function body
}
Explanation:
- auto Keyword: The return type will be specified later using the trailing return arrow (->), and this keyword serves as a placeholder for it.
- function_name: It is the name of the function that you select.
- parameter_list: It is the list, delimited by parenthesis, of the function's parameters.
- return_type: The function's actual return type, trailing after the return arrow.
As an illustration:
Simple Function with a Basic Return Type:
auto add(int x, int y) -> int {
return x + y;
}
Function with a Nested Return Type:
auto find_max_element(std::vector<int>& vec) -> std::vector<int>::iterator {
return std::max_element(vec.begin(), vec.end());
}
Function Using decltype for Type Deduction:
template <typename T>
auto create_vector(size_t size) -> std::vector<T> {
return std::vector<T>(size);
}
Use Cases and Examples of Trailing Return Types
Trailing return types excel when it comes to complex type expressions, template metaprogramming, and scenarios where the return type is dependent on function arguments or template parameters. Here are some important situations in which they perform well:
- Template Metaprogramming:
Simplifying Nested Type Dependencies:
#include <iostream>
#include <vector>
template <typename T>
auto get_iterator_type(T& container) -> typename T::iterator {
return container.begin();
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
auto iterator = get_iterator_type(vec);
std::cout << "The first element in the container is: " << *iterator << std::endl;
return 0;
}
Output:
Explanation:
- The template argument T's iterator member type determines the return type.
- By avoiding repetition, the trailing return type clarifies this dependency.
- Conditional Return Types:
Selecting Return Type Based on Conditions:
auto create_reference(bool use_ref) -> std::conditional_t<use_ref, int&, int> {
int value = 42;
// ... logic to decide reference or value
return value; // Return type determined based on condition
}
- The use_ref argument is used to determine the return type at compile time.
- This conditionality is handled gracefully by the trailing return type.
- Functions Using decltype for Type Deduction:
Deducing Return Type from Expressions:
template <typename T1, typename T2>
auto add_and_multiply(T1 x, T2 y) -> decltype(x + y * 2) {
return x + y * 2;
}
- Decltype is used to infer the return type from the returned expression.
- This type of deduction can be enabled without explicitly writing it out by using the trailing return type.
- Lambda Expressions:
Specifying Return Types for Lambdas:
#include <iostream>
int main() {
auto even_numbers = [](int x) -> bool {
return x % 2 == 0;
};
// Testing the lambda function
int number = 10;
if (even_numbers(number)) {
std::cout << number << " is even." << std::endl;
} else {
std::cout << number << " is odd." << std::endl;
}
return 0;
}
Output:
- Lambda expressions inherently require trailing return types.
Auto Keyword and Trailing Return Type: A Powerful Combination:
The auto keyword in C++ is essential for enabling return types that trail. Together, they simplify function declarations in the following ways:
- auto as a Placeholder:
- The auto keyword acts as a stand-in for the actual return type when trailing return types are used; the actual return type is specified later using the trailing return arrow (->).
- The compiler should infer the return type from the function's body or other context.
- Type Deduction:
- The compiler identifies the proper return type by examining how the function is implemented.
- Expressions inside the function body are considered during this deduction process, which may employ decltype in more complex situations.
- Enhanced Readability:
- It is usually easier to read when the return type comes after the parameter list, especially for long or complex types.
- It enables you to deal with the parameters and name of the function before addressing the return type.
- Flexibility and Conciseness:
- When the return type may be variable or unknown, auto in the return type provides flexibility.
- It can result in shorter code that does not require long type names to be explicitly written out.
Examples:
Simple Function with Type Deduction:
#include <iostream>
auto add(int x, int y) -> decltype(x + y) {
return x + y; // Compiler deduces return type as int
}
int main() {
int a = 5;
int b = 7;
// Using the add function
auto result = add(a, b);
// Displaying the result
std::cout << "The sum of " << a << " and " << b << " is: " << result << std::endl;
return 0;
}
Output:
Template Function with Dependent Return Type:
#include <iostream>
#include <vector>
template <typename T>
auto create_vector(size_t size) -> std::vector<T> {
return std::vector<T>(size);
}
int main() {
auto intVector = create_vector<int>(5);
auto doubleVector = create_vector<double>(8);
std::cout << "Size of intVector: " << intVector.size() << std::endl;
std::cout << "Size of doubleVector: " << doubleVector.size() << std::endl;
return 0;
}
Output:
Compatibility and Portability: Navigating the Transition to Trailing Return Types
Although trailing return types have many advantages, you should take compatibility and portability into account before implementing them in your C++ projects:
- Compatibility with Older C++ Standards:
- Trailing return types are a relatively new addition to the standard.
- Not Compatible with Previous Compilers: Compilation errors result from compilers that don't fully support C++11. These compilers won't recognize return types that trail.
- Compatibility and Portability:
- Discuss how trailing return types work with older standards.
- Discuss any possible problems or things to remember while converting code to C++11 and utilizing trailing return types.
- Project-Specific Requirements:
- Analyze the needs of the project: Examine your project's particular constraints and requirements about the portability and support for the C++ standard.
- Select an Adoption Plan: Choose whether to fully embrace trailing return types or use them sparingly after your evaluation.
- Potential Issues:
- Compiler Support: To prevent incompatibilities, make sure your compiler fully supports C++11.
- Code Readability: Developers used to traditional syntax may find trailing return types strange. Give precise instructions and illustrations to make adoption easier.
- Debugging: The debugging experience may be impacted by certain debuggers' incorrect display of trailing return types.
Real-World Examples of Trailing Return Types in Action:
Here are some sample code from well-known open-source projects that shows how trailing return types are used in real-world scenarios:
- Eigen (Linear Algebra Library):
template<typename BinaryOp>
auto cwiseProduct(const MatrixBase<OtherDerived>& other, const BinaryOp& func) const
-> const CwiseBinaryOp<BinaryOp, const Derived, const OtherDerived>
{
return CwiseBinaryOp<BinaryOp, const Derived, const OtherDerived>(derived(), other.derived(), func);
}
Explanation:
- This example uses a trailing return type when the complex return type of a template function depends on the template arguments.
- By arranging the return type after the function's parameter list, readability is improved.
- Boost.Asio (Asynchronous I/O Library):
template <typename CompletionToken>
auto async_read_until(AsyncReadStream& s,
asio::basic_streambuf<Allocator>& b,
char delim, CompletionToken&& token)
-> typename asio::async_result<
typename std::decay<CompletionToken>::type,
void(error_code, std::size_t)>::return_type
{
return asio::async_initiate<CompletionToken,
void(error_code, std::size_t)>(
typename asio::detail::initiate_async_read_until_delim_v1<
AsyncReadStream,
asio::basic_streambuf<Allocator>,
char>::type(), token, s, b, delim);
}
Explanation:
- This example uses trailing return type for a complex template function involving asynchronous operations.
- Leverages decltype to deduce the return type based on the completion token, ensuring type safety.
- Range-v3 (Range Library):
template<typename T>
constexpr auto next(T& t)
-> decltype(std::next(std::declval<T&>()))
{
return std::next(t);
}
Explanation:
- This example demonstrates how to infer the return type for a range adaptor from the following expression by using decltype and a trailing return type.
- It makes it easier for generic code to function with different range types.
- Clang (C++ Compiler Frontend):
template <typename T>
auto make_unique(Args&&... args) -> std::unique_ptr<T> {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
Explanation:
- It provides a template function that generates an instance of std::unique_ptr with a trailing return type.
- It removes the need for the template argument explicitly specified in the return type, improving readability and maintainability.