Default template arguments

A template parameter can have a default argument:

template<typename T = int>
void foo();

If the type is not specified explicitly, the default argument is used:

foo();         // T = int
foo<double>(); // T = double

In the second call, the explicitly specified argument replaces the default.

Defaults that depend on earlier parameters

A default argument can depend on preceding template parameters:

#include <type_traits>

template<typename T1, typename T2,
         typename RT = std::common_type_t<T1, T2>>
RT max_value(T1 a, T2 b)
{
    return b < a ? a : b;
}

The types T1 and T2 are deduced from the function arguments, while RT is determined by std::common_type_t:

auto a = max_value(4, 7.2);
// T1 = int
// T2 = double
// RT = double

The return type can also be specified explicitly. To do that, you must specify the preceding template arguments as well:

auto b = max_value<int, double, long double>(4, 7.2);
// RT = long double

Putting the return type first

In a function template, a parameter with a default argument can appear before parameters that are deduced from the function arguments:

template<typename RT = long, typename T1, typename T2>
RT max_value(T1 a, T2 b)
{
    return b < a ? a : b;
}

This makes it convenient to specify the return type first:

max_value(4, 7.2);         // RT = long, result is 7
max_value<double>(4, 7.2); // RT = double, result is 7.2

In both calls, the compiler deduces T1 = int and T2 = double. Only the return type differs.

Overloading a function template

A function template can have the same name as an ordinary function:

int max_value(int a, int b)
{
    return b < a ? a : b;
}

template<typename T>
T max_value(T a, T b)
{
    return b < a ? a : b;
}

Consider these calls:

max_value(7, 42);     // Ordinary function
max_value(7.0, 42.0); // Template with T = double
max_value('a', 'b');  // Template with T = char

Arguments of type int

max_value(7, 42);

Both candidates are suitable for this call:

  • the ordinary function int max_value(int, int);
  • the template specialization max_value<int>.

Both match the argument types exactly. When the conversion sequences are equally good, the ordinary function is preferred.

Arguments of type double

max_value(7.0, 42.0);

The ordinary function requires each argument to be converted:

double → int

The template with T = double is an exact match, so max_value<double> is selected.

Arguments of type char

max_value('a', 'b');

The template with T = char accepts the arguments without changing their type. The ordinary function requires a conversion from char to int, so the template is selected.

Overloading with arguments of different types

For the same set of overloads, consider a call with arguments of different types:

max_value('a', 42.7);

Both function parameters in the template have the same type T:

template<typename T>
T max_value(T, T);

But the arguments produce conflicting deduction results:

First argument  → T = char
Second argument → T = double

A single type for T cannot be deduced, so this template candidate is discarded.

The ordinary function remains viable:

int max_value(int, int);

To call it, both arguments are converted to int:

char   → int
double → int

Thus, deduction failure for one template candidate does not necessarily make the entire call invalid: another overload may still work.

Calling max_value(), max_value<>(), and max_value<T>()

For a set containing an ordinary function and a template with one parameter T, these three forms have different meanings:

max_value(7, 42);
max_value<>(7, 42);
max_value<double>(7, 42);

Ordinary call

max_value(7, 42);

The entire available overload set is considered: ordinary functions and function templates. Here, the ordinary function with int parameters is selected.

Empty template argument list

max_value<>(7, 42);

The choice is made among function templates. The parameter T is deduced automatically:

T = int

Explicit type

max_value<double>(7, 42);

The type is specified in advance:

T = double

Both int arguments are converted to double.

Multiple function template overloads

Function templates can also be overloaded:

template<typename T>
T max_value(T a, T b)
{
    return b < a ? a : b;
}

template<typename T>
T* max_value(T* a, T* b)
{
    return *b < *a ? a : b;
}

The first version compares the values passed to it. The second takes pointers, compares the objects they point to, and returns one of the pointers.

Consider this call:

int a = 10;
int b = 20;

int* p1 = &a;
int* p2 = &b;

auto p = max_value(p1, p2);

Both versions are viable.

General template

template<typename T>
T max_value(T, T);

The compiler deduces T = int*. After substitution, the parameter and return types are:

int* max_value(int*, int*);

Pointer template

template<typename T>
T* max_value(T*, T*);

The compiler deduces T = int. After substitution, this version has the same parameter and return types:

int* max_value(int*, int*);

To choose between them, the compiler compares how specialized the templates are. The version with T* parameters is more specialized and is selected.

This mechanism is called partial ordering of function templates.

As a result, p points to b, because 20 is greater than 10.

An overload for C strings

Comparing pointers with < does not compare string contents. The overload that dereferences *a and *b does not solve the problem either: for character pointers, it compares only the first characters.

To compare C strings lexicographically, we can add a separate overload:

#include <cstring>

char const* max_value(char const* a, char const* b)
{
    return std::strcmp(b, a) < 0 ? a : b;
}

The complete overload set now looks like this:

#include <cstring>
#include <string>

template<typename T>
T max_value(T a, T b)
{
    return b < a ? a : b;
}

template<typename T>
T* max_value(T* a, T* b)
{
    return *b < *a ? a : b;
}

char const* max_value(char const* a, char const* b)
{
    return std::strcmp(b, a) < 0 ? a : b;
}

Selecting overloads for different types

int a = 7;
int b = 42;
auto m1 = max_value(a, b); // 42

std::string s1 = "hey";
std::string s2 = "you";
auto m2 = max_value(s1, s2); // "you"

int* p1 = &a;
int* p2 = &b;
auto m3 = max_value(p1, p2); // Pointer to b

char const* x = "hello";
char const* y = "world";
auto m4 = max_value(x, y); // Pointer to "world"

Each call selects the appropriate overload:

  • for int and std::string, the template with T parameters;
  • for int*, the template with T* parameters;
  • for char const*, the ordinary function that uses std::strcmp.

Different ways of passing and returning values

Combining overloads that return by reference with overloads that return by value requires care with object lifetimes.

Consider a two-argument template that returns a reference:

template<typename T>
T const& max_value(T const& a, T const& b)
{
    return b < a ? a : b;
}

For C strings, keep an overload that takes and returns pointers by value:

#include <cstring>

char const* max_value(char const* a, char const* b)
{
    return std::strcmp(b, a) < 0 ? a : b;
}

After these overloads, define a function for three values:

template<typename T>
T const& max_value(T const& a,
                   T const& b,
                   T const& c)
{
    return max_value(max_value(a, b), c);
}

Calling it with numbers

auto m = max_value(7, 42, 68); // m = 68

Here, the result is copied into m while the temporary arguments still exist.

Calling it with C strings

char const* s1 = "frederic";
char const* s2 = "anica";
char const* s3 = "lucas";

auto m = max_value(s1, s2, s3); // Lifetime error

Here, the three-argument function returns a dangling reference. The cause lies in the results of the nested calls.

Where the dangling reference comes from

Consider the expression inside the three-argument template:

return max_value(max_value(a, b), c);

For arguments of type char const*, the following happens:

  1. The inner call max_value(a, b) selects the C-string overload and returns a pointer by value.
  2. The outer two-argument call also selects the C-string overload and returns a pointer by value.
  3. The three-argument template must return T const&, so its return reference binds to a temporary pointer object.

In this case, T = char const*, so the return reference type is:

char const* const&

In C++17, returning this reference does not extend the temporary object's lifetime. The reference dangles after the return expression completes. Using it to initialize m causes undefined behavior.

Returning the result by value

In this example, we can avoid the problem by returning by value from every overload:

#include <cstring>

template<typename T>
T max_value(T a, T b)
{
    return b < a ? a : b;
}

char const* max_value(char const* a, char const* b)
{
    return std::strcmp(b, a) < 0 ? a : b;
}

template<typename T>
T max_value(T a, T b, T c)
{
    return max_value(max_value(a, b), c);
}

The C-string overload is declared before the three-argument template is defined, so it is available when the nested calls are resolved.

The result for C strings is now a copy of the selected pointer:

char const* s1 = "frederic";
char const* s2 = "anica";
char const* s3 = "lucas";

auto m = max_value(s1, s2, s3); // Pointer to "lucas"

Declaration order

The overloads available inside a template depend on where that template is defined.

In this example, the ordinary function is declared after the three-argument template:

#include <iostream>

template<typename T>
T max_value(T a, T b)
{
    std::cout << "template\n";
    return b < a ? a : b;
}

template<typename T>
T max_value(T a, T b, T c)
{
    return max_value(max_value(a, b), c);
}

int max_value(int a, int b)
{
    std::cout << "int overload\n";
    return b < a ? a : b;
}

This call:

max_value(47, 11, 33);

uses the two-argument template inside the three-argument template. The output is:

template
template

Why the later overload is not selected

The reason involves two-phase name lookup.

When the three-argument template is defined, the compiler can already see the two-argument template:

template<typename T>
T max_value(T, T);

The ordinary function:

int max_value(int, int);

is declared later.

When dependent calls are instantiated, the candidate set can be extended through ADL—argument-dependent lookup.

The type int has no associated namespaces, so the later global overload is not added to the candidate set.

Making the overload available

To make the ordinary function participate in overload resolution inside the three-argument template, declare it earlier:

#include <iostream>

int max_value(int a, int b);

template<typename T>
T max_value(T a, T b)
{
    std::cout << "template\n";
    return b < a ? a : b;
}

template<typename T>
T max_value(T a, T b, T c)
{
    return max_value(max_value(a, b), c);
}

int max_value(int a, int b)
{
    std::cout << "int overload\n";
    return b < a ? a : b;
}

Now the same call:

max_value(47, 11, 33);

produces:

int overload
int overload

The earlier declaration makes the ordinary function visible when the template is defined. During overload resolution for int arguments, that function is preferred.