The Challenge of Generic Programming
In traditional programming, functions are designed to operate on specific data types. If you wanted to create a function to add two numbers, you might write one for integers, another for doubles, and so forth. This approach leads to code duplication, reduced maintainability, and limits the reusability of your functions.
int addIntegers(int a, int b) {
return a + b;
}
double addDoubles(double a, double b) {
return a + b;
}
Function Templates
Function templates in C++ address the limitations of traditional programming by allowing you to write generic functions that can work with a variety of data types. They provide a mechanism for creating templated functions, which automatically adapt to the data types they are invoked with.
A function template is defined using the template
keyword followed by a template parameter list enclosed in angle brackets (<>
). The template parameters can be types (typename
or class
), non-type parameters, or template parameters.
template <typename T> // this is the template parameter declaration
T add(T a, T b) { // this is the function template definition for add<T>
return a + b;
}
In this example, the add
function template takes two parameters of type T
(a generic type), and the addition operation is applied to these parameters. The typename
keyword indicates that T
is a type parameter.
Using Function Templates
To use a function template, you provide the template arguments explicitly or let the compiler deduce them based on the function arguments.
Syntax:
add<actual_type>(arg1, arg2); // actual_type is some actual type, like int or double
int main() {
int result1 = add<int>(3, 5); // Explicit template argument
double result2 = add(3.5, 2.7); // Template argument deduction
return 0;
}
In this example, add<int>
explicitly specifies the template argument int
, while add(3.5, 2.7)
relies on template argument deduction.
Function Template with Non-Template Parameters
It's possible to create function templates that have both template parameters and non-template parameters. The type template parameters an be matched to any type, and the non-template parameters work like the parameters of normal functions.
For example:
// T is a type template parameter
// double is a non-template parameter
template <typename T>
int someFcn (T, double)
{
return 5;
}
int main()
{
someFcn(1, 3.4); // matches someFcn(int, double)
someFcn(1, 3.4f); // matches someFcn(int, double) -- the float is promoted to a double
someFcn(1.2, 3.4); // matches someFcn(double, double)
someFcn(1.2f, 3.4); // matches someFcn(float, double)
someFcn(1.2f, 3.4f); // matches someFcn(float, double) -- the float is promoted to a double
return 0;
}
This function template has templated first parameter, but the second parameter is fixed with type double
. In this case, our function will always return an int
value.
Function Template in Multiple Files
Template Declaration (Header File: mytemplate.h
):
- Define the template function in a header file, which includes only the declarations.
// mytemplate.h
#ifndef MYTEMPLATE_H
#define MYTEMPLATE_H
template <typename T>
T add(T a, T b);
#endif // MYTEMPLATE_H
Template Definition (Source file: mytemplate.cpp
):
- Provides the definitions of the template functions in a separate source file.
// mytemplate.cpp
#include "mytemplate.h"
template <typename T>
T add(T a, T b) {
return a + b;
}
// Explicit instantiation for int to avoid linker errors
template int add<int>(int, int);
Note: The explicit instantiation is added to the source file to avoid linker errors when the template is used with specific types.
Main Program (Source file: main.cpp
):
- Use the template function in your main program.
// main.cpp
#include <iostream>
#include "mytemplate.h"
int main() {
int result = add(3, 5);
std::cout << "Result: " << result << std::endl;
double resultDouble = add(3.5, 2.7);
std::cout << "Result (double): " << resultDouble << std::endl;
return 0;
}
Compilation:
- Compile both the template source file and the main program source file.
g++ -c mytemplate.cpp -o mytemplate.o
g++ -c main.cpp -o main.o
g++ mytemplate.o main.o -o myprogram
Function Templates with Multiple Template Types
Consider the following program:
#include <iostream>
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(2, 3.5) << '\n'; // compile error
return 0;
}
You may be surprised to find that this program won't compile.
In our function call max(2, 3.5)
, we are passing arguments of two different types: one of int
and one double
.
Use static_cast to convert the arguments to matching types:
The first solution is to put the burden on the caller to convert the arguments into matching types. For example:
#include <iostream>
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(static_cast<double>(2), 3.5) << '\n'; // convert our int to a double so we can call max(double, double)
return 0;
}
Now that both arguments are of type double
, the compiler will be able to instantiate max(double, double)
that will satisfy this function call.
However, this solution is awkward and hard to read.
Provide an explicit type template argument
If we had written a non-template max(double, double)
function, then we would be able to call max(int, double)
and let the implicit type conversion rules convert our int
argument into a double
so the function call could be resolved:
#include <iostream>
double max(double x, double y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(2, 3.5) << '\n'; // the int argument will be converted to a double
return 0;
}
However, when the compiler is doing template argument deduction, it won't do any type conversions. Fortunately, we don't have to use template argument deduction if we specify an explicit type template argument to be used instead:
#include <iostream>
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
int main()
{
// we've explicitly specified type double, so the compiler won't use template argument deduction
std::cout << max<double>(2, 3.5) << '\n';
return 0;
}
In the above example, we call max<double>(2, 3.5)
. Because we have explicitly specified T
should be replaced with double
. Our int
parameter will be implicitly converted to a double
.
While this is more readable than using static_cast
, it would be even nicer if we didn't even have to think about the types when making a function call to max
at all.
Function templates with multiple template type parameters:
The root of our problem is that we have only defined single template type (T
) for our function template, and then specified that both parameters must be of this same type.
The best way to solve this problem is to rewrite our function template in such a way that our parameters can resolve to different type. Rather than using one template type parameter T
, we will not use two (T
and U
).
#include <iostream>
template <typename T, typename U> // We're using two template type parameters named T and U
T max(T x, U y) // x can resolve to type T, and y can resolve to type U
{
return (x < y) ? y : x; // uh oh, we have a narrowing conversion problem here
}
int main()
{
std::cout << max(2, 3.5) << '\n';
return 0;
}
Because we have defined x
with template T
, and y
with template type U
, x
and y
can now resolve their types independently. When we call max(2, 3.5)
, T
can be an int
, and U
can be a double
. The compiler will happily instantiate max<int, double>(int, double)
for us.
However, the above code still has a problem: using the usual arithmetic rules, double
takes precedence over int
, so our conditional operator will return a double
. But our function is defined as returning a T
– in cases where T
resolves to an int
, our double
return value will undergo a narrowing conversion to an int
, which will produce a warning (and possible loss of data).
Making the return type a U
instead doesn't solve the problem, as we can always flip the order of the operands in the function call to flip the types of T
and U
.
How do we solve this ? This is a good use for an auto
return type – we will let the compiler deduce what the return type should be from the return statement:
#include <iostream>
template <typename T, typename U>
auto max(T x, U y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(2, 3.5) << '\n';
return 0;
}
This version of max
now works fine with operands of different types.
Abbreviated Function Templates [C++20]
C++20 introduces a new use of the auto
keyword: When the auto
keyword is used as a parameter type in a normal function, the compiler will automatically convert the function into a function template with each auto parameter becoming an independent template type parameter. This method for creating a function template is called an abbreviated function template
.
For example:
auto max(auto x, auto y)
{
return (x < y) ? y : x;
}
is shorthand in C++ for the following:
template <typename T, typename U>
auto max(T x, U y)
{
return (x < y) ? y : x;
}
which is the same as the max
function template we wrote above.