This chapter will discuss the various ways we can declare or deduce the return type of a template function. We can use either template or non-template parameters to declare the return type of a function template. We can also deduce the return type using keywords like auto and decltype. In some cases, we can declare a separate type parameter for the return type. Because of these intricacies, it is necessary to discuss the template function’s return type in detail with specific examples tailored for each case.
In this chapter, we will cover the following topics:
- Using non type parameter for return type
- Using type parameter for return type
- Declaring separate type parameter for return type
- Deducing return type using auto
- Deducing return type using auto and decltype
- Using std::common_type for return type
Techical Requirements
To follow the examples from this book, you’ll need the following tools:
- Most of the example in this chapter will work with standard C++11 compiler.
- Some of the examples in this chapter will require support of C++14 or higher compiler.
The code files for this chapter can be found on GitHub at:
Using non-template parameter for return type
Like any other ordinary functions, the template functions can have a non-template return type, i.e., any built-in type (int, double, char, etc.) or user-defined data type (struct or class type). In the following section, we will see an example of the built-in type used as the return type.
Using Built-in type as return type
The following example demonstrates any built-in data type can be used as the return type of a template function:
#include <iostream>
template<typename T>
bool is_equal(T x, T y)
{
return x == y;
}
int main()
{
int var1 = 10, var2 = 20;
std::cout << " var1 and var2 "
<< (is_equal(var1, var2) ? " are " : " are not ")
<< " equal" << std::endl;
return 0;
}
As you can see in this program, we have defined a template function with a single template parameter T, two function parameters x and y, of type T. The return type is of type bool. The template function is_equal() is returning the comparison result of x and y; if they are equal, then is_equal() returns true; otherwise, it returns false.
We have used the relation/comparison operator (==) to compare x and y. The comparison operator returns true if both sides of the operator are equal; else, it returns false. So, the return value of the comparison operator is Boolean (true/false). Template functions can have a void type as a return type as well. In the next section, we will see how void can be used as the return type for a template function.
Using void as return type
This section demonstrates a function template that does not return anything; hence the return type is void. Let us have a look at the following program:
#include <iostream>
#include <vector>
template<typename T>
void add_to_store(std::vector<T>& store, T val)
{
store.push_back(val);
}
int main()
{
std::vector<int> data_store;
int val = 10;
add_to_store(data_store, val);
return 0;
}
This program has defined a function template with a single template parameter T, two type parameters store and val. The first parameter store is a reference to an STL vector container. The second parameter val is a variable of type T. When we call add_to_store(), we pass an STL vector container that can hold type T values and a value of type T to be inserted into the container. In the template function body, the value we pass is pushed back into the vector. This program is a hypothetical implementation of a custom data store. The following section will demonstrate how to use a type parameter as the template function return type.
Using type parameter for return type
We can use the type parameter in the return type of a template function as well. To understand this better, let us have a look at the following example:
#include <iostream>
template<typename T>
T increment(T& x)
{
return ++x;
}
int main()
{
double dval = 10.5;
std::cout << "double incr = " << incremet(dval) << std::endl;
int ival = 20;
std::cout << "int incr = " << incremet(ival) << std::endl;
return 0;
}
As you can see in this example, we have defined a function template increment() with a single type parameter T. The type parameter T has been used to declare the function parameter x and declare the template function’s return type. In the main(), we passed dval, which is of type double, and ival, which of type int. The function template increment() uses the unary increment operator (++) and returns the incremented value. As the type of return value is the same as that of x, the type parameter T can be used to denote the template function’s return type. However, there may be a situation when we need to declare a separate template parameter to denote the type of the return. In the next section, we will learn how to do that.
Declaring separate type parameter for return type
In some situations, you may have to declare a separate type parameter for the return type. Our previous strategy will not work in scenarios where the call parameter’s deduced type differs from the return type. In the following section, we will learn about how to declare and use a separate template type parameter for the return type.
Working with array argument
Let us write a function template that takes an array of int or double numbers and returns the largest element in the array.
Incorrect return type declaration
First, we will take a naive approach to write this function template. Then we will see what the problems are in that approach and then fix those issues to reach our final solution. Let us try to define the function template like the following:
template<typename T>
T largest_elem(T number_set, size_t len)
{
T largest = number_set[0];
for(size_t index = 0; index < len; ++index)
if (number_set[index] > largest)
largest = number_set[index];
return largest;
}
We have defined a template function largest_elem() with a single type parameter T. Notice, T has been used to declare the function parameter number_set (which is an array in this case) and a non-template parameter len which is of type int.
Now we need to decide the return type of this template function. The elements of the array can be either int or double. Hence, we have decalred a generic type T to represent either int or double. The function template returns the largest of all the elements. Depending on what the array contains, the return type could be either int or double. So, you might think we can use T to denote the return type. Let us try use fit the template function in the following program and try to compile it and see what happens:
#include <iostream>
template<typename T>
T largest_elem(const T number_set, size_t len)
{
T largest = number_set[0];
for(size_t index = 0; index < len; ++index)
if (number_set[index] > largest)
largest = number_set[index];
return largest;
}
int main()
{
double numbers[] = {20.1, 10.1, 30.1, 5.1, 40.1, 200.1, 50.1, 1.1, 0.5, 100.5};
double l = largest_elem(numbers, 10);
std::cout << "largest = " << l << std::endl;
return 0;
}
If we try to compile the program, we’ll get the following:
$ g++ return_type_error.cpp -o return_type_error -Wall -Werror
return_type_error.cpp: In function 'int main()':
return_type_error.cpp:15:40: error: cannot convert 'double*' to 'double' in initialization
double l = largest_elem(numbers, 10);
return_type_error.cpp: In instantiation of 'T largest_elem(T, size_t) [with T = double*; size_t = long unsigned int]':
return_type_error.cpp:15:40: required from here
return_type_error.cpp:5:7: error: cannot convert 'double' to 'double*' in initialization
T largest = number_set[0];
return_type_error.cpp:7:31: error: invalid operands of types 'double' and 'double*' to binary 'operator>'
if (number_set[index] > largest)
return_type_error.cpp:8:21: error: cannot convert 'double' to 'double*' in assignment
largest = number_set[index];
As you can see, the compiler is throwing errors. Let us take a closer look at the following error line:
return_type_error.cpp:15:40: error: cannot convert 'double*' to 'double' in initialization
The compiler is saying it cannot convert double * to double.
Array decay phenomenon
The reason is, when we pass an array to a template function, the array decays to a pointer. This topic has been discussed in Chapter 3 : All about Template Parameters in C++ in detail. In this case, the compiler has deduced T’s type to be double * as we have passed an array of doubles to the function template. This can be seen from the following error line:
return_type_error.cpp: In instantiation of 'T largest_elem(T, size_t) [with T = double*; size_t = long unsigned int]':
Interpreting the error message
In this error message, the compiler is telling us what it has deduced T to be. The array numbers have decayed into a pointer to an object of type double. We expected the compiler to deduce T to be double and hence declared the return type to be double. However, as we have used T to declare the return type, the compiler has found a contradiction. From the template function body, the compiler could deduce the return type to be double. Hence, the use of T as the return type has created a conflict. Therefore, the compiler is complaining that it cannot convert double * to double. To resolve this problem, we must declare a separate type parameter for the return type.
The correct implementation
Let us have a look at the following modified program:
#include <iostream>
template<typename T, typename U>
U largest_elem(const T number_set, size_t len)
{
U largest = number_set[0];
for(size_t index = 0; index < len; ++index)
if (number_set[index] > largest)
largest = number_set[index];
return largest;
}
int main()
{
double numbers[] = {20.1, 10.1, 30.1, 5.1, 40.1, 200.1, 50.1, 1.1, 0.5, 100.5};
double l = largest_elem<double [], double>(numbers, 10);
std::cout << "largest = " << l << std::endl;
return 0;
}
In this program, we have declared an additional type parameter U, and we have declared the return type as U. Inside the function template, we have declared a variable largest of type U. In the main() function, we have called the function template as shown next:
double l = largest_elem<double [], double>(numbers, 10);
As you can see, while calling the function template, we have specified two types, double [] and double within angle brackets (<>). The first type, double [] represents the array we are passing to the function template, and the second type, double is representing the return type of the function template U. Now, let us compile the program as the following:
$ g++ explicit_return_type.cpp -o explicit_return_type -Wall -Werror
$ ./explicit_return_type
largest = 200.1
We need to specify U’s type because the compiler cannot deduce the return type from the function template call. In this example, the template function call parameter was an array. The mechanism works pretty much the same for any standard data type. In the following section, we will learn how to declare and use a separate type parameter for a non-array argument.
Working with non-array argument
In the previous section, we learned how an array type argument required an explicit template parameter for the return type. We may require an explicit template parameter for non-array type argument as well. Let us look at the following program:
#include <iostream>
template<typename T, typename U>
U get_coef(T type)
{
std::cout << type << std::endl;
U ret;
switch(type) {
case 1:
ret = 20.5;
break;
case 2:
ret = 100.5;
break;
default:
ret = 10.4;
}
return ret;
}
int main()
{
int type = 2;
std::cout << get_coef<int, double>(type) << std::endl;
return 0;
}
Here, we have defined a template function get_coef() which returns a hypothetical coefficient for a particular object category (represented by the type variable). The type in this example is an int type variable, whereas the returned value of the get_coef(), represented by the ret variable, is of type double.
The type variable represents a specific object category, e.g., it could be a hardware type or a firmware version, or any other object type. get_coef() should take an object category (type) and return the coefficient for that category.
The data type of the ret variable (the coefficient) could also vary. In this example, for the sake of simplicity, we have hardcoded the coefficient values. However, in a real situation, the values may come from an external source like a data file. For example, the template function may read the values from a data file and return it to the caller. Imagine you are reading these coefficient values from an external data file. Depending on the source, the data type could be different for different source files. It is possible that, when you read from one type of data file, the data type (the values of the coefficients) may be int, whereas when you read from another type of data file, the data type could be double, and so on.
Return type specification
Now let us examine how we are controlling the return type of the template function from the main. We are calling the template function as shown next:
get_coef<int, double>(type)
Here, we have specified two different data types within the angle bracket:
- First, the data type of the template argument, that is, type of the variable named “type”.
- Second, the data type of the return value of the template function get_coef().
Order is important
We previously learned that the compiler could deduce the type(s) of template argument(s) automatically from the template function call. So ideally, we should be able to drop the type of the argument from the angle bracket and get away by only specifying the return value type. Let us try this in the following program:
#include <iostream>
template<typename T, typename U>
U get_coef(T type)
{
std::cout << type << std::endl;
U ret;
switch(type) {
case 1:
ret = 20.5;
break;
case 2:
ret = 100.5;
break;
default:
ret = 10.4;
}
return ret;
}
int main()
{
int type = 2;
std::cout << get_coef<double>(type) << std::endl;
return 0;
}
In this program, we have specified only the return type of the template function get_coef() in the angle brackets while calling get_coef() assuming that the compiler will able to automatically deduce the template function argument by itself from the passed values. Let us now try to compile the program as follows:
$ g++ explicit_return_type_3.cpp -o explicit_return_type_3 -Wall -Werror
explicit_return_type_3.cpp:3:3: note: template argument deduction/substitution failed:
explicit_return_type_3.cpp:23:39: note: couldn't deduce template parameter 'U'
As you can see, the compilation failed, template argument deduction/substitution failed, compiler complaining that it cannot deduce the template parameter U. So, what happened? The problem here is the order of template parameters in the parameter list. Let us look at the template parameter list again as the following:
template<typename T, typename U>
The order of the template parameters in the declaration is first T, and then U. Now, let us see how we are calling the get_coef() template function:
std::cout << get_coef<double>(type) << std::endl;
We have explicitly specified only double within the angle brackets. When the compiler looks at the get_coef() call, it substitutes T with double but cannot substitute for the U. As discussed before, compiler cannot deduce the return type of the template function from the template function call. That is why it doesn’t know how to substitute U and hence we see the compilation failure.
Lets fix the order
To be able only to specify the return type and leave the template function argument deduction to the compiler, we need to change the order of the template parameter list as follows:
template<typename U, typename T>
The complete program is given in the following listing:
#include <iostream>
template<typename U, typename T>
U get_coef(T type)
{
std::cout << type << std::endl;
U ret;
switch(type) {
case 1:
ret = 20.5;
break;
case 2:
ret = 100.5;
break;
default:
ret = 10.4;
}
return ret;
}
int main()
{
int type = 2;
std::cout << get_coef<double>(type) << std::endl;
return 0;
}
As you can see in this program, we have swapped the order of the template parameters T and U. As the type parameter U has been declared first, the compiler can substitute U with the specified type double (explicitly specified within angle brackets). Then the compiler deduces the type of the variable type (data type of the variable type is int here) and replaces T with the deduced type (int). As a result, if we compile the program now as the following, there will be no more error:
$ g++ explicit_return_type_4.cpp -o explicit_return_type_4 -Wall -Werror
$ ./explicit_return_type_4
2
100.5
The template function’s return type can also be left unspecified and automatically deduced by the compiler using the keyword auto. In the next section, we will see how that works.
Deducing return type using auto
The compiler can automatically deduce the return type of the function template if we use the keyword auto. In this case, the compiler looks at the template function body and automatically figures out the return value type from the return statement. In the following section, we will see how to use the auto keyword for declaring the return type of the template function.
Deducing return type from auto variable
Let us look at the following example code:
#include <iostream>
template<typename T>
auto get_coef(T type)
{
auto ret = 0.0;
switch(type) {
case 1:
ret = 20.5;
break;
case 2:
ret = 100.50;
break;
default:
ret = 200.3;
}
return ret;
}
int main()
{
int type = 2;
std::cout << get_coef(type) << std::endl;
return 0;
}
In this program, we defined a template function get_coef() and specified the return type of the template function as auto. The keyword auto tells the compiler that it must deduce the template function’s return type from the return statement inside the function body. Notice that we have declared a variable, called ret, in the function template body using auto and initialized it with the value 0.0. Remember, the auto variable’s initialization is crucial here. The compiler would have no clue what the type of the variable would be if we had not initialized it with a value. The initialization value tells the compiler what the type of the ret variable is. Now let us compile the program as the following:
$ g++ auto_return_type.cpp --std=c++14 -o auto_return_type -Wall -Werror
$ ./auto_return_type
100.5
The initialization of the auto variable needs some special attention here. In the following section, we will learn the importance of initializing the auto variable.
Initialization of auto variable
In this example, we have passed a value of 2 in the template function call and received a return value of 100.5 (which is double). This is because the compiler has deduced the type of the variable ret from the initialization statement as double. However, if we initialize the ret variable with an int value instead of a double value, the output will be different (erroneous). If we initialize ret with an int value, the compiler will deduce the type of ret as int. Consequently, the following assignment will be erroneous and will lead to data truncation:
ret = 100.50;
As we have changed the initialization value of ret to a value of int, the type of ret will be deduced as int. As a result, it cannot hold a double value of 100.50 as doubles need larger memory space than int. Consequently, data truncation will take place. In this case, the output of this program will be 100 instead of an expected 100.5 because of data truncation (data loss). So, appropriate initialization of the ret variable is critical in this case. In this example, we learned how the compiler could deduce the return type from an auto variable, ret variable, in this case. However, the compiler is capable of directly deducing the return type from the return statement itself. In the next section, we will learn how to do that.
Deducing return type from return statement
The compiler can directly deduce the return type from the return statement itself. Let us look at the following program:
#include <iostream>
template <typename T>
auto select_band(T x)
{
return (x > 0 && x < 100) ? 1 : 2;
}
int main()
{
std::cout << select_band(100) << std::endl;
return 0;
}
Now, if we compile and run the program, you will see the output as follows:
$ g++ auto_return_type_2.cpp --std=c++14 -o auto_return_type_2 -Wall -Werror
$ ./auto_return_type_2
2
As you can see, the compiler has deduced the return type from the return statement itself, and no explicit auto variable declaration and initialization was necessary. However, we need to note here that using auto to deduce the return type by the compiler is only supported in C++14 or higher version. In the next section, we will learn about the compiler dependency in using auto.
Understanding the compiler dependency
In the previous two programs, you might have noticed that we have specified –std=c++14 in the command line while compiling the program. The reason is using auto in this fashion is supported in C++14 and above. So, if your compiler does not have the support of C++14, you will get an error. To verify that this is not supported in C++11, you can change the compile version from C++14 to C++11 as the following, and it will throw an error:
$ g++ auto_return_type_2.cpp --std=c++11 -o auto_return_type_2 -Wall -Werror
auto_return_type_2.cpp:3:21: error: 'select_band' function uses 'auto' type specifier without trailing return type
auto select_band(T x)
auto_return_type_2.cpp:3:21: note: deduced return type only available with -std=c++14 or -std=gnu++14
In this case, we have just changed the command line argument –std to c++11, which has caused the compilation error. In the next section, we will learn how to use auto with the C++11 compiler.
Deducing return type using auto and decltype
If we examine the error messages from the last example, we will see the following line:
auto_return_type_2.cpp:3:21: error: 'select_band' function uses 'auto' type specifier without trailing return type
What it is telling us is that the trailing type for the auto type specifier is missing. In C++11, the auto mechanism works in this fashion only if it has a trailing decltype keyword. To understand this, let us look at the following program:
#include <iostream>
template <typename T>
auto select_band(T x) -> decltype((x > 0 && x < 100) ? 1 : 2)
{
return (x > 0 && x < 100) ? 1 : 2;
}
int main()
{
std::cout << select_band(100) << std::endl;
return 0;
}
We have used the decltype keyword to deduce the type of return value from an expression. The following expression has been passed to the decltype, and the rest is left to the compiler to deduce:
-> decltype((x > 0 && x < 100) ? 1 : 2)
The compiler looks at the template function declaration and sees the return type specified as auto. The auto keyword tells the compiler that it must deduce the return type from a trailing decltype. The compiler looks at the expression passed in the decltype and deduces the required type from the expression. The operator -> is the part of the semantics and needs to be specified to denote the trailing decltype. In the next section, we will learn how to declare the return type of a template function using std::common_type.
Using std::common_type for return type
In C++14, we can use std::common_type to deduce a common type from a list of types and use it to declare the template function’s return type. Sometimes, you may have a requirement to return different types of data from a template function. This requirement is application specific. We will try to understand the mechanism to select a common type from a list of types specified by template parameters. In the template function body, we would return different types of data depending on specific conditions. Let us consider the following program:
common_type_as_return_type.cpp
#include <iostream>
#include<cstdlib>
#include <unistd.h>
int get_dummy_process_output()
{
sleep(1);
srand((unsigned) time(NULL));
int random = rand() % 10;
return random;
}
template<typename T, typename U>
std::common_type_t<T, U> get_some_value(T x, U y)
{
if(get_dummy_process_output() < 5)
return x;
else
return y;
}
int main()
{
for( auto i=0; i < 5; ++i)
std::cout << get_some_value(10, 200.56) << std::endl;
return 0;
}
In this program, we have defined a template function get_some_value(). The get_some_value() template function has two template parameters T and U, and it has two function parameters, x, and y, which are of type T and U, respectively. In the template function body, we call another function called get_dummy_process(), which returns a random value between 0 and 10. If the value returned by get_dummy_process() is less than 5, we will return x; otherwise y. The template function’s return type has been declared using std::common_type_t, which takes a list of template parameters within angle brackets.
In this case, the template parameters that have been passed to std::common_type are T and U. What std::common_type tells the compiler is that the template function may return either a data of type T or U. So, we can return two different types of data from the template function. This program is effectively not doing much. But you hopefully get the idea, and in a situation where you will have a real requirement to write a similar code, you will have an example to refer. You can compile and run the program as shown next:
$ g++ common_type_as_return_type.cpp --std=c++14 -o common_type_as_return_type -Wall -Werror
200.56
10
200.56
10
200.56
Remember, this is a C++14 feature, and you need to have a compiler that can support C++14. Notice the use of the –std==c++14 argument, which specifies the compiler to be used as c++14. The same code will not work with the c++11 compiler. To prove that, you can change the compiler type to C++11 with the help of the –std argument, as shown in the following:
$ g++ common_type_as_return_type.cpp --std=c++11 -o common_type_as_return_type -Wall -Werror
common_type_as_return_type.cpp:12:6: error: 'common_type_t' in namespace 'std' does not name a template type
As you can see, because this time we have changed the compiler from C++14 to C++11, the compiler can no longer recognize the common_type_t.
Summary
In this chapter, we learned in detail the various ways we can declare the return type of a template function. First, we saw the usual return types, which are non-template template parameters. These can constitute the built-in data types or the user-defined data types. Then we learned about how to use the type parameter as the return type. Then we explored scenarios where declaring a separate type parameter for the return type may be necessary. We also explored how to use the auto keyword to direct the compiler to deduce the return type automatically from the return statements. We saw the difference in uses of the auto when it comes to C++11 and C++14. Finally, we learned how to deduce a common return type from a list of template parameters. We have come a long way so far, and you did well. In the next chapter, we will see some miscellaneous features of template functions.
Questions
- The return type of a template function must be declared using template parameter. [True/False]
- Only using the keyword auto is sufficient to declare the return type of a template function in C++11. [True/False]
- What keyword you must use in conjunction with auto to declare the return of a template function in C++11?
- Declaring a separate type parameter for specifying the return type of a template function is mandatory in C++. [True/False]
- A template function must return a value and hence the use of void as return type is not possible. [True/False]
- If we provide an expression to the keyword decltype, it gives us back the type of that expression. [True/False]
- Given that we declare the template function appropriately, it is possible to explicitly specify the return type of a template function using angle brackets while calling the template function. [True/False]
- It is possible to declare a variable as auto without initializing it. [True/False]
- When a variable is declared using the keyword auto in C++, the compiler deduces the type of the variable examining the value it has been initialized with. [True/False]
- What would be output of the following program?
#include <iostream>
int main()
{
auto var = 10;
var = 20.5;
std::cout << "var = " << var << std::endl;
return 0;
}
- 20.5
- 20
- Compilation error
- None of the above.
Answers
- False
- False
- decltype
- False
- False
- True
- True
- False
- True
- b
Leave a Reply