Chapter 4: Return Types of Template Functions in C++

Explicit, deduced, and common return types — from auto and decltype to std::common_type

Vivek Bhadra  ·  C++11 / C++14  ·  Templates  ·  Return Type Deduction  ·  Generic Programming

This chapter explores multiple methods for declaring or deducing the return type of a template function in C++. The return type can be specified explicitly using template parameters or inferred from non-template arguments. By leveraging keywords such as auto and decltype, the compiler can deduce the return type, simplifying function definitions and increasing code flexibility. In some scenarios, introducing a dedicated template parameter for the return type provides additional control. Each of these approaches influences the structure and usability of template functions in practical programming. Throughout this chapter, focused examples will illustrate each technique, reflecting common scenarios encountered in real-world software development.

In this chapter, we will explore the following topics related to declaring and deducing the return type of a template function:

Technical Requirements

To follow the examples in this chapter, you will need the following tools:

  • A standard C++11 compiler to run most of the examples.
  • A C++14 or higher compiler for some advanced examples requiring extra language features.
  • Access to the source code for this chapter, available on GitHub: 🔗 Source Code – Chapter 4

Make sure you have the necessary compiler version and development environment set up before running the examples.

Using non-template parameter for return type

Just like ordinary functions, template functions can have a non-template return type. The return type can be any built-in type. Examples include int, double, and char. It can also be a user-defined type, like a struct or class.

In the next section, we will explore an example. A built-in type is used as the return type in a function template.

Using Built-in type as return type

The following example demonstrates how a 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;
}

We have defined a template function in this program with a single template parameter T. The function takes two parameters, x and y, both of type T, and returns a Boolean (bool) value.

The template function is_equal() compares x and y using the comparison operator (==). If both values are equal, it returns true; otherwise, it returns false. The comparison operator (==) evaluates both operands and returns true when they match or false when they differ.

Since template functions allow flexible return types, they can also have a void return type. In the next section, we will explore how to use void as the return type for a template function in C++.

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 next 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. We also pass 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 show 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 = " << increment(dval) << std::endl;
    int ival = 20;
    std::cout << "int incr = " << increment(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 is of type int. The function template increment() uses the unary increment operator (++) and returns the incremented value. The type of return value is the same as that of x. Hence, the type parameter T can be used to denote the template function’s return type. But, there can be times when declaring a separate template parameter is necessary. This separate template parameter would denote the return type. In the next section, we will learn how to do that.

Declaring separate type parameter for return type

In some situations, you have to declare a separate type parameter for the return type. Our earlier strategy will not work in scenarios where the call parameter’s deduced type differs from the return type. In the next section, we will learn how to declare a separate template type parameter. We will also learn how to use it for the return type.

Working with Array Arguments in a C++ Template Function

Let’s write a function template. It will take an array of int or double values. It will return the largest element in the array.

Incorrect Return Type Declaration

To start, we will implement a straightforward version of the function template. Next, we will examine the shortcomings of this approach. After identifying those issues, we’ll apply improvements to achieve a robust final solution. Let’s begin by defining the initial function template below.

template<typename T>
T largest_elem(T data_set, size_t count)
{
    T max_value = data_set[0];
    for (size_t i = 0; i < count; ++i)
        if (data_set[i] > max_value)
            max_value = data_set[i];
    return max_value;
}

We have defined a template function largest_elem() with a single type parameter T. Here, T is used to declare the function parameter data_set, which represents the array passed to the function. There is also a non-template parameter count, which indicates how many elements the array holds.

The idea behind this design is simple. The function should examine the elements stored in data_set and return the largest one. Since the array may contain values of type int or double, the type parameter T is intended to represent whichever element type appears in the array. At first sight, using T as the return type seems reasonable, because the function is expected to return one of the elements.

However, this assumption leads to a problem that is not immediately obvious. To see where the difficulty arises, we will place this function inside a small test program, compile it, and observe the compiler’s response. This helps us understand why the seemingly natural choice of return type does not work in practice.

#include <iostream>

template<typename T>
T largest_elem(const T data_set, size_t count)
{
    T max_value = data_set[0];
    for (size_t i = 0; i < count; ++i)
        if (data_set[i] > max_value)
            max_value = data_set[i];
    return max_value;
}

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 largest = largest_elem(numbers, 10);
    std::cout << "largest = " << largest << "\n";

    return 0;
}

When we compile this program, we immediately run into a series of errors:

return_type_error.cpp: In function 'int main()':
error: cannot convert 'double*' to 'double' in initialization
    double largest = largest_elem(numbers, 10);
                                  ^
return_type_error.cpp: In instantiation of 'T largest_elem(T, size_t) [with T = double*]':
error: cannot convert 'double' to 'double*' in initialization
    T max_value = data_set[0];
                   ^
error: invalid operands of types 'double' and 'double*' to binary 'operator>'
        if (data_set[i] > max_value)

As you can see, the compiler is throwing errors. Let us take a closer look at the following error line:

error: cannot convert 'double*' to 'double' in initialization
    double largest = largest_elem(numbers, 10);

The compiler is saying it cannot convert double* to double. Based on our expectation, the compiler should have deduced T as double because we passed an array of doubles. So the natural question is, why has the compiler deduced T as double* instead of double? In the next section, we will walk through what actually happened inside the program.

Array decay phenomenon

The reason for this behaviour is that when an array is passed to a function parameter that is not declared as an array reference, it is automatically converted to a pointer to its first element. This conversion is part of the core language rules. In a template function, this conversion becomes visible during type deduction, which is why the compiler deduces T as double* instead of double. This behaviour was discussed earlier in Chapter 3: All about Template Parameters in C++. In our case, we passed an array of doubles. As a result, the compiler has deduced the type T as double*. This deduction is clearly shown in the following part of the error message:

return_type_error.cpp: In instantiation of 'T largest_elem(T, size_t) [with T = double*]'

A related error follows immediately:

error: cannot convert 'double' to 'double*' in initialization
    T max_value = data_set[0];

The compiler is indicating that it is treating T as double* and therefore expects max_value to hold a pointer to double. However, data_set[0] is a double, and assigning it to a double pointer is invalid. This mismatch occurs entirely because the array has been converted to a pointer during the function call.

The correct implementation

Let us have a look at the following modified program:

// return_type_correct.cpp
#include <iostream>

template<typename T, typename U>
U largest_elem(const T data_set, size_t count)
{
    U largest = data_set[0];
    for (size_t i = 0; i < count; ++i)
        if (data_set[i] > largest)
            largest = data_set[i];
    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 result = largest_elem<double[], double>(numbers, 10);

    std::cout << "largest = " << result << "\n";
    return 0;
}

In this version of the program, we introduce a second type parameter U, and the function now returns a value of type U. Inside the function, the variable largest is also declared as type U. This allows us to separate the type of the input array from the type of the returned value.

The function template is called in main() as shown below:

double l = largest_elem<double [], double>(numbers, 10);

Here, the call provides two explicit template arguments. The first argument, double [], represents the type of the array being passed to the function template. The second argument, double, represents the return type and corresponds to the template parameter U. By supplying both types directly, we avoid the earlier type deduction issue.

This separation ensures that the compiler does not attempt to deduce the array parameter as a pointer. It avoids using that pointer type as the return type. Now, let us compile and run the program to confirm that the earlier issue has been resolved.

$ g++ explicit_return_type.cpp -o explicit_return_type -Wall -Werror
$ ./explicit_return_type
largest = 200.1

Key Notes to Remember

  • When an array is passed to a function template, the compiler faces a challenge. It cannot deduce the intended return type from the function call.
  • A separate type parameter such as U must be explicitly specified to indicate the return type.
  • Introducing a distinct template parameter for the return type gives the programmer full control and avoids deduction errors.

The next section explains how to use a dedicated return type parameter even when the argument is not an array.

Working with non-array argument

In the earlier section, we learned how an array type argument required an explicit template parameter for the return type. We 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;
}

We have defined a template function get_coef(). It returns a hypothetical coefficient for a particular object category. This is represented by the type variable. The type in this example is an int type variable. The returned value of get_coef(), represented by the ret variable, is of type double.

The type variable signifies a specific object category, e.g., it is 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 hard-coded the coefficient values. However, in a real situation, the values come from an external source like a data file. For example, the template function can 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 is different for different source files. It is possible that, when you read from one type of data file, the data type may be int. When you read from another type of data file, the data type may 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, the type of the variable named “type”.
  • Second, the data type of the return value of the template function get_coef().

Order is important

We earlier learned that the compiler can deduce the type(s) of template argument(s) automatically from the template function call. Ideally, we should be able to drop the type of the argument from the angle bracket. We should 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. We call get_coef(). We assume that the compiler can deduce the template function argument automatically 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 and substitution failed. The compiler is 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:

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 can’t substitute for U. As discussed before, the compiler can’t 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.

Let’s 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 entire program is given in the next 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. The type parameter U is declared first. Thus, the compiler can substitute U with the specified type double. This type is explicitly specified within angle brackets. Then the compiler deduces the type of the variable type. The data type of the variable type is int here. It replaces T with the deduced type int. As a result, if we compile the program now, 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. It automatically figures out the return value type from the return statement. In the following section, we will learn about using the auto keyword. We will discuss 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 instructs the compiler to deduce the template function’s return type from the return statement inside the function body. Notice that we have declared a variable in the function template body using auto. The variable is called ret. It is initialized with the value 0.0. Remember, the auto variable’s initialization is crucial here. The compiler would have no idea about the type of the variable 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:

$ 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. We 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 use an int value to initialize the ret variable, the output will be different. It will be erroneous. If we initialize ret with an int value, the compiler will deduce the type of ret as int. Therefore, the following assignment will be erroneous and will lead to data truncation:

        ret = 100.50;

We changed the initialization value of ret to a value of int. Therefore, 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. 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

The compiler has deduced the return type from the return statement itself. As you can see, 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. 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 that 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 follows, 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:

#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 data of type T or U. So, we can return two different types of data from the template function. 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 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 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. In the next chapter, we will see some miscellaneous features of template functions.

Questions

  1. The return type of a template function must be declared using template parameter. [True/False]
  2. Only using the keyword auto is sufficient to declare the return type of a template function in C++11. [True/False]
  3. What keyword you must use in conjunction with auto to declare the return of a template function in C++11?
  4. Declaring a separate type parameter for specifying the return type of a template function is mandatory in C++. [True/False]
  5. A template function must return a value and hence the use of void as return type is not possible. [True/False]
  6. If we provide an expression to the keyword decltype, it gives us back the type of that expression. [True/False]
  7. 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]
  8. It is possible to declare a variable as auto without initializing it. [True/False]
  9. 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]
  10. What would be the output of the following program?
#include <iostream>
int main()
{
    auto var = 10;
    var = 20.5;
    std::cout << "var = " << var << std::endl;
    return 0;
}
  1. 20.5
  2. 20
  3. Compilation error
  4. None of the above.

Answers

  1. False
  2. False
  3. decltype
  4. False
  5. False
  6. True
  7. True
  8. False
  9. True
  10. b

Leave a Reply