Consider the following program:
int Max(int x, int y) {
return x > y ? x : y;
}
std::string Max(std::string x, std::string y) {
return x > y ? x : y;
}
int main() {
using namespace std;
cout << Max(3, 4) << ";" << Max( string{"abc"}, string{"xyz"} ) << endl;
}
It generates the following output:
4;xyz
The Max() function is overloaded to support comparing ints and
std::strings. Given that operator>() is defined for both types, the
implementation is actually the same. In C++, we can templatize the
implementation by its type so that we can deduplicate the code:
template <typename T>
T Max(T x, T y) {
return x > y ? x : y;
}
The template definition isn’t real code. When the compiler sees the callsites
Max(3, 4) and Max( string{"abc"}, string{"xyz"} ), it’ll try to find
overloads for a function called Max() with parameter types that match the
callsites. When it doesn’t find any overloads, it sees the template definition
and generates concrete function implementations from the template instead. Two
versions of Max() will be generated: one takes two integers, the other takes
two strings. A function template defines a family of functions that the
compiler can generate based on what callsites exist in the program.
By the way, notice how we’re taking the arguments x and y and returning the
result by value. If T is some C++ object, we’ll be invoking the copy
constructor for these three things. It would be better to specify const T& for
all three because we’re not mutating anything and we’re not creating a new
object to return:
template <typename T>
const T& Max(const T& x, const T& y) {
return x > y ? x : y;
}
Next, let’s consider what happens when we add the following invocation of the
Max() function to our program:
cout << Max("AAA", "BBB") << endl;
This is not the same as the std::string invocation; this will cause the
compiler to generate a new Max() implementation that will compare two
const char* types.
When we try to compile the new program, the compiler actually emits a warning:
In instantiation of ‘const T& Max(const T&, const T&) [with T = char [4]]’:
...
warning: comparison between two arrays is deprecated in C++20 [-Warray-compare]
| return x > y ? x : y;
| ~~^~~
...
The compiler is warning you about trying to compare two arrays, but as was the case in C, the comparison will decay into comparing the memory addresses of the first element in each array. Pointer comparison is undefined when the two pointers don’t refer to elements in the same array; the placement of arrays in memory by the compiler is arbitrary.
Surely enough, our output is incorrect (although it could have been arbitrarily correct as well):
4;xyz
AAA
It seems that our function template that simply invokes operator>()
won’t work for char*. What do we do if we have a function template
implementation but need to adjust the definition for a particular type? We can
simply provide a non-template overload for the particular type.
Declaring a non-template overload of the Max() function by listing its
prototype before the call site will prevent the compiler from generating an
instance of the function from the function template:
const char* Max(const char* x, const char* y);
...
cout << Max("AAA", "BBB") << endl;
We can then provide the following definition specialized for the const char*
type in a separate .cpp file that we compile and link into the program:
const char* Max(const char* x, const char* y) {
return strcmp(x, y) > 0 ? x : y;
}
Or we can put the function in the same compilation unit as the call site by
limiting its scope to the current file by using the static keyword, either
directly as shown below or through a .h file:
// Non-template overload specialized for C-strings
static inline const char* Max(const char* x, const char* y) {
return strcmp(x, y) > 0 ? x : y;
}
...
cout << Max("AAA", "BBB") << endl;
Using the non-template overload specialized for const char*,
the program now computes Max("AAA", "BBB") correctly:
4;xyz
BBB
When a function template and a non-template function are overloaded, and a call can resolve to either of them, the compiler will give preference to the non-template function.
Template specialization is another way to provide a specialized implementation for a particular type. Here is an example to illustrate the syntax:
// A function template with a type parameter T
template <typename T>
void func(T t) {}
// Specialization of func() with T == int
template <>
void func<int>(int i) {}
// Another specialization with T == double
// We can omit "<double>" and let the compiler deduce it
template <>
void func(double d) {}
Template specialization is used a lot for class templates (you can templatize classes just like functions, and we will learn it soon), but it is not used much for function templates because overloading with non-template functions as we described above is simpler and better.
One reason why function template specialization is difficult to use is because
it requires an exact match of the template arguments to the specialization’s
signature. Not even array-to-pointer decay is allowed. For example, if we were
to write a specialization to make Max("AAA","BBB") work, we have to write the
following:
// Our Max() function template
template <typename T>
const T& Max(const T& x, const T& y) {
return x > y ? x : y;
}
// Declare FourChars as a synonym for char[4]
typedef char FourChars[4];
// Specialization of Max() with T == char[4]
template <>
const FourChars& Max(const FourChars& x, const FourChars& y) {
return strcmp(x, y) > 0 ? x : y;
}
...
cout << Max("AAA", "BBB") << endl;
So far, we’ve been putting the template defintion in the same source file as our
main() function. If we were to properly organize our code, you may think that
we should break out our Max() implementation into a header and source file
like this:
// max.h
#ifndef __MAX_H__
#define __MAX_H__
template <typename T>
const T& Max(const T& x, const T& y);
#endif
// max.cpp
template <typename T>
const T& Max(const T& x, const T& y) {
return x > y ? x : y;
}
After all, we’ve always broken up our source code by placing prototypes in
header files and the implementation in source files. Anyone that wants to use
our Max() template can just include our max.h to see the prototype in order
to compile and then link with our max.o to get the implementation.
Recall from earlier, however, that we said that a template definition is not
real code. It’s just an outline used by the compiler to generate real code
on-the-fly as it sees callsites of the Max() function. If we compile our
hypothetical max.cpp by itself, it’ll basically be an empty object file; the
compiler won’t generate template code unless it sees the template being used.
As such, we have to define our template directly into the header file max.h:
// max.h
#ifndef __MAX_H__
#define __MAX_H__
template <typename T>
const T& Max(const T& x, const T& y) {
return x > y ? x : y;
}
#endif
Any source code files that call the Max() function need to include our max.h
header file to bring in the template definition in order to compile their code.
What happens when you have multiple source files that use the same template
definition? Let’s consider func1.cpp and func2.cpp, two source files that
both use the Max() template:
// func1.cpp
#include "max.h"
int func1(int x, int y) {
return Max(x, y); // will instantiate Max(int,int)
}
// func2.cpp
#include "max.h"
int func2(int x, int y) {
return Max(x, y); // will instantiate Max(int,int)
}
We’ll now call func1() and func2() from our max-test.cpp program:
int main() {
using namespace std;
int func1(int, int); // defined in func1.cpp
int func2(int, int); // defined in func2.cpp
cout << func1(5, 6) << ";" << func2(7, 8) << endl;
}
When func1.cpp and func2.cpp are separately compiled, they will each
generate Max(int, int). Let’s take a look inside of func1.o and func2.o to
verify this. The objdump -d command will display the assembler contents of the
given object file:
$ objdump -d func1.o
func1.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z5func1ii>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 10 sub $0x10,%rsp
c: 89 7d fc mov %edi,-0x4(%rbp)
f: 89 75 f8 mov %esi,-0x8(%rbp)
12: 48 8d 55 f8 lea -0x8(%rbp),%rdx
16: 48 8d 45 fc lea -0x4(%rbp),%rax
1a: 48 89 d6 mov %rdx,%rsi
1d: 48 89 c7 mov %rax,%rdi
20: e8 00 00 00 00 call 25 <_Z5func1ii+0x25>
25: 8b 00 mov (%rax),%eax
27: c9 leave
28: c3 ret
Disassembly of section .text._Z3MaxIiERKT_S2_S2_:
0000000000000000 <_Z3MaxIiERKT_S2_S2_>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
c: 48 89 75 f0 mov %rsi,-0x10(%rbp)
10: 48 8b 45 f8 mov -0x8(%rbp),%rax
14: 8b 10 mov (%rax),%edx
16: 48 8b 45 f0 mov -0x10(%rbp),%rax
1a: 8b 00 mov (%rax),%eax
1c: 39 c2 cmp %eax,%edx
1e: 7e 06 jle 26 <_Z3MaxIiERKT_S2_S2_+0x26>
20: 48 8b 45 f8 mov -0x8(%rbp),%rax
24: eb 04 jmp 2a <_Z3MaxIiERKT_S2_S2_+0x2a>
26: 48 8b 45 f0 mov -0x10(%rbp),%rax
2a: 5d pop %rbp
2b: c3
$ objdump -d func2.o
func2.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z5func2ii>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 10 sub $0x10,%rsp
c: 89 7d fc mov %edi,-0x4(%rbp)
f: 89 75 f8 mov %esi,-0x8(%rbp)
12: 48 8d 55 f8 lea -0x8(%rbp),%rdx
16: 48 8d 45 fc lea -0x4(%rbp),%rax
1a: 48 89 d6 mov %rdx,%rsi
1d: 48 89 c7 mov %rax,%rdi
20: e8 00 00 00 00 call 25 <_Z5func2ii+0x25>
25: 8b 00 mov (%rax),%eax
27: c9 leave
28: c3 ret
Disassembly of section .text._Z3MaxIiERKT_S2_S2_:
0000000000000000 <_Z3MaxIiERKT_S2_S2_>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
c: 48 89 75 f0 mov %rsi,-0x10(%rbp)
10: 48 8b 45 f8 mov -0x8(%rbp),%rax
14: 8b 10 mov (%rax),%edx
16: 48 8b 45 f0 mov -0x10(%rbp),%rax
1a: 8b 00 mov (%rax),%eax
1c: 39 c2 cmp %eax,%edx
1e: 7e 06 jle 26 <_Z3MaxIiERKT_S2_S2_+0x26>
20: 48 8b 45 f8 mov -0x8(%rbp),%rax
24: eb 04 jmp 2a <_Z3MaxIiERKT_S2_S2_+0x2a>
26: 48 8b 45 f0 mov -0x10(%rbp),%rax
2a: 5d pop %rbp
2b: c3 ret
First, notice the names of the functions: _Z5func1ii(), _Z5func2ii, and
_Z3MaxIiERKT_S2_S2_. Since C++ introduced function overloading with different
parameter types, a function name is no longer unique. There could be several
versions of a function with the same name that take different types of
parameters. To disambiguate function names, the C++ compiler performs
name-mangling to ensure each function overload is named uniquely according
to its type signature.
Second, we can see that both object files indeed contain the same implementation
of Max(int, int), named _Z3MaxIiERKT_S2_S2_. When we link func1.o and
func2.o together, shouldn’t the linker complain about the duplicate
definitions of _Z3MaxIiERKT_S2_S2_()? This is the case for normal functions,
but template functions are special. The compiler makes a note for the linker
that it may see multiple definitions of the template function and that it should
just choose one of them for the final executable.
If you’re familiar with the ELF binary format, we can dive deeper and see how
the compiler and linker handle duplicate template instantiations. Let’s take a
look at the symbol table for func1.o:
$ readelf --symbols func1.o
Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS func1.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 2 .text
3: 0000000000000000 0 SECTION LOCAL DEFAULT 6 .text._Z3MaxIiER[...]
4: 0000000000000000 0 SECTION LOCAL DEFAULT 7 .debug_info
5: 0000000000000000 0 SECTION LOCAL DEFAULT 9 .debug_abbrev
6: 0000000000000000 0 SECTION LOCAL DEFAULT 12 .debug_rnglists
7: 0000000000000000 0 SECTION LOCAL DEFAULT 14 .debug_line
8: 0000000000000000 0 SECTION LOCAL DEFAULT 16 .debug_str
9: 0000000000000000 0 SECTION LOCAL DEFAULT 17 .debug_line_str
10: 0000000000000000 41 FUNC GLOBAL DEFAULT 2 _Z5func1ii
11: 0000000000000000 44 FUNC WEAK DEFAULT 6 _Z3MaxIiERKT_S2_S2_
The Max(int, int) instantiation of the Max() function template,
_Z3MaxIiERKT_S2_S2_(), has a WEAK binding. When the linker is creating the
final executable and only sees weak bindings for Max(int, int), it arbitrarily
chooses one and throws away the rest.
Modules, introduced in C++20, changed this model of template compilation, but compiler support is not widely available at the time of this writing.
Last updated: 2026-02-09