In our earlier lessons on template functions, we introduced scenarios where we don’t necessarily know what the return type of the instantiated function will be. This is because the specific return type will depend on the type of the arguments.
In many simple scenarios, we can get around this simply by setting the return type to auto
, asking the compiler to figure it out based on our return
 statements:
#include <iostream>
template <typename T1, typename T2>
auto Multiply(T1 x, T2 y){ return x * y; }
int main(){
std::cout << "Double: " << Multiply(2.4, 3.2);
std::cout << "\nInt: " << Multiply(2, 3);
}
Double: 7.68
Int: 6
But in more complex scenarios, this is not enough.
Let's consider a simple, contrived addition to our function where we want to return a zero-initialized object based on a conditional check:
#include <iostream>
template <typename T1, typename T2>
auto Multiply(T1 x, T2 y){
if (x == 0 || y == 0) return 0;
return x * y;
}
int main(){
std::cout << "Double: " << Multiply(2.4, 3.2);
}
In this situation, we now have one branch of our generated function returning an int
, whilst another returns a double
. With a defined return type, this is not necessarily a problem. The compiler can cast what is returned by the return
statement into the function’s returned type.
But, with an auto
return type, the compiler no longer knows what object it needs to return. And, when different branches of our function return different types, it doesn’t know what to do and throws an error:
all return expressions must deduce to the same type
We could try solving this problem by coercing our return value to one of our template types. For example, we could explicitly call a type constructor in our return statement:
template <typename T1, typename T2>
auto Multiply(T1 x, T2 y) {
if (x == 0 || y == 0) return T1(0);
return x * y;
}
Or we could replace the auto
return type with one that matches one of our template parameters:
template <typename T1, typename T2>
T1 Multiply(T1 x, T2 y) {
if (x == 0 || y == 0) return 0;
return x * y;
}
This will let our program compile, but now the return type of our multiplication function, and therefore its arithmetic value, depends on the order of our parameters:
#include <iostream>
template <typename T1, typename T2>
T1 Multiply(T1 x, T2 y){
if (x == 0 || y == 0) return 0;
return x * y;
}
int main(){
std::cout << "First: " << Multiply(2.1, 2);
std::cout << "\nSecond: " << Multiply(2, 2.1);
}
First: 4.2
Second: 4
Additionally, these techniques aren’t always an option. They require our return type to be the same as one of our argument types.
That is not always possible. Were we to be multiplying matrices, the type of matrix that is returned is not necessarily the same type as either of the arguments.
For example, multiplying a 3x1 matrix by a 1x3 matrix results in a 3x3 matrix, which would be an entirely different type in almost any implementation:
int main(){
Matrix<3, 1> A{1, 2, 3};
Matrix<1, 3> B{4, 5, 6};
Matrix<3, 3> C{Multiply(A, B)};
}
So, we need a better way to solve this problem.
decltype()
In our earlier lessons, we introduced decltype
, which lets us deduce a type at compile time, based on an expression.
In this excessively contrived example, we show how we can use decltype
to construct a type based on the types of our arguments:
auto Multiply(int x, int y) {
using ReturnType = decltype(x * y);
return ReturnType { x * y };
}
We can also remove the intermediate type alias if preferred:
auto Multiply(int x, int y) {
return decltype(x * y) { x * y };
}
Within regular functions, this is rarely useful, but it becomes very helpful when writing function templates. For example, it can be used to solve the problem we introduced at the start of this section:
#include <iostream>
template <typename T1, typename T2>
auto Multiply(T1 x, T2 y) {
if (x == 0 || y == 0) {
return decltype(x * y){0};
}
return x * y;
}
int main(){
std::cout << "First: " << Multiply(2.1, 2);
std::cout << "\nSecond: " << Multiply(2, 2.1);
auto ReturnValue{Multiply(0.0, 0.0)};
std::cout << "\nZero-Initialization: "
<< ReturnValue
<< " ("
<< typeid(decltype(ReturnValue)).name()
<< ")";
}
First: 4.2
Second: 4.2
Zero-Initialization: 0 (double)
This works correctly, however, it is somewhat indirect. If we know the return type of a function, we’d ideally like that return type to be part of the function signature. Were we to attempt this decltype
technique with the regular function syntax, we’d run into an issue, as the return type comes before the parameter list:
decltype(x * y) Multiply(int x, int y) {
return x * y;
}
This results in the usual compilation error we’d get from trying to use an identifier before it was declared:
'x': undeclared identifier
'y': undeclared identifier
So, we need an alternative syntax that moves the function’s return type after the parameter list.
The following shows a minimalist example of a function that uses a trailing return type. We use the auto
keyword in place of the traditional return type. We then then place the return type between the function’s parameter list and the body, using an arrow ->
 syntax.
auto GetNumber() -> int {
return 42;
}
When our function has parameters, it looks like this:
auto Add(int x, int y) -> int {
return x + y;
}
The benefit of the trailing return type is that it now has access to those parameters. This lets the compiler infer the return type based on those values:
auto Add(int x, int y) -> decltype(x + y) {
return x + y;
}
The trailing return type syntax is most useful within template functions, where we don’t necessarily know the parameter types when we’re creating the template function.
The following example fixes our earlier problem, by allowing us to correctly determine the return type based on an expression involving our parameters:
#include <iostream>
template <typename T1, typename T2>
auto Multiply(T1 x, T2 y) -> decltype(x * y){
if (x == 0 || y == 0) return 0;
return x * y;
}
int main(){
std::cout << "First: " << Multiply(2.1, 2);
std::cout << "\nSecond: " << Multiply(2, 2.1);
auto ReturnValue{Multiply(0.0, 0.0)};
std::cout << "\nZero-Initialization: "
<< ReturnValue
<< " ("
<< typeid(decltype(ReturnValue)).name()
<< ")";
}
First: 4.2
Second: 4.2
Zero-Initialization: 0 (double)
Naturally, this also scales to more complex types. Below, we’ve added a new class. Without modifying our template function at all, it still works as expected, returning a type that doesn’t match either of the argument types but is still derived from them.
Note that the following Matrix
class template is nowhere near complete, as that’s not our focus. It is only implemented to the extent that demonstrates the behavior of our Multiply
template function.
The class also uses static
variables, which may be unfamiliar at this stage. We have a dedicated lesson on later in the course:
#include <iostream>
template <int R, int C>
class Matrix {
public:
static const int Rows{R};
static const int Cols{C};
// Constructors
Matrix(){}
Matrix(int){}
// Operators
bool operator==(int){ return true; }
template <typename T>
auto operator*(T Other){
return Matrix<Rows, Other.Cols>{};
}
};
template <typename T1, typename T2>
auto Multiply(T1 x, T2 y) -> decltype(x * y){
if (x == 0 || y == 0) return 0;
return x * y;
}
int main(){
Matrix<3, 1> A;
Matrix<1, 3> B;
auto Result{Multiply(A, B)};
std::cout
<< "Rows: " << Result.Rows
<< "\nColumns: " << Result.Cols;
}
Rows: 3
Columns: 3
Within class methods, we can use trailing return types in the same way.
class Character {
public:
auto GetName() -> std::string {
return Name;
}
private:
std::string Name;
};
However, there are some interactions with qualifiers such as const
, override
, and final
we need to note.
The const
qualifier needs to go before the return type, whilst override
and final
appear after:
class Character {
public:
virtual auto GetName() const -> std::string {
return Name;
}
virtual auto GetLevel() const -> int {
return Level;
}
private:
std::string Name;
int Level;
};
class Monster : public Character {
public:
auto GetName() const -> std::string override {
// do monster things, then...
return Character::GetName();
}
auto GetLevel() const -> int final {
// do monster things, then...
return Character::GetLevel();
}
};
Within the community, the use of the trailing return type is gaining traction as the default way of defining functions, even in scenarios where it is not required.
There are two main arguments that supporters of the convention use.
Firstly, having the parameter types before the return type is often a more intuitive way of ordering information. It maps to how we typically think about functions ("this function accepts x and returns y") and also how functions behave (they use their parameters, and then they return).
The second reason is that, within a long list of function declarations, using a trailing return type causes the function names to visually align. This can make scanning class definitions to understand their capabilities a more pleasant experience:
// Before
class Character {
public:
const std::string& GetName() const;
void SetName(std::string);
int GetLevel() const;
void SetLevel(int);
std::shared_ptr<Party> GetParty() const;
void SetParty(std::shared_ptr<Party>);
};
// After
class Character {
public:
auto GetName() const -> const std::string&;
auto SetName(std::string) -> void;
auto GetLevel() const -> int;
auto SetLevel(int) -> void;
auto GetParty() const -> std::shared_ptr<Party>;
auto SetParty(std::shared_ptr<Party>) -> void;
};
This example also shows the main drawback of the trailing return type syntax - it simply requires more code. This is mostly due to the requirement that we need to include the auto
keyword at the start of every declaration.
As such, there is generally no agreed recommendation here. Most teams will have their own agreed standard, otherwise, we should feel free to use whichever pattern we prefer.
This lesson introduced trailing return types, enabling more flexible function declarations, especially in templates and complex type deduction scenarios. The key topics we learned include:
auto
and ->
.auto
return type deduction in functions with conditional or complex returns.decltype
in conjunction with trailing return types to accurately deduce and specify return types in template functions.const
, override
, and final
.An alternative syntax for defining function templates, which allows the return type to be based on their parameter types
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.