Templates in Assembly
Templates vanish at compile time: each instantiation is a separate, fully concrete function, often with a mangled name.
The Function That Produces Nothing
Put this in a file by itself and compile it to assembly:
template <typename T>
T maxOf(T a, T b)
{
return (a < b) ? b : a;
}
The listing is empty. Not a stripped-down function, not a stub: no instructions at all. Compare that with an ordinary function, which always produces a label and a body, and the difference is stark. A template is not a function. It is a recipe the compiler keeps on hand, waiting for you to ask for a concrete version. Until then there is nothing it could compile: should a < b become a cmp on integer registers or a comisd on xmm registers? That depends entirely on what T turns out to be.
The first rule of templates in assembly is therefore simple: a template that is never instantiated produces no assembly whatsoever.
Each Instantiation Is Its Own Function
Now give the compiler a reason to use the recipe:
int biggerInt(int a, int b)
{
return maxOf(a, b);
}
double biggerDouble(double a, double b)
{
return maxOf(a, b);
}
The call in biggerInt forces the compiler to instantiate maxOf<int>; the call in biggerDouble forces maxOf<double>. Each instantiation is a complete, fully concrete function with its own label and its own body:
int maxOf<int>(int, int):
cmp edi, esi
mov eax, esi
cmovge eax, edi
ret
double maxOf<double>(double, double):
comisd xmm0, xmm1
ja .L2
movsd xmm0, xmm1
.L2:
ret
Read both with the eyes you built in earlier chapters. The int version is pure Chapter 3: arguments in edi and esi, a cmp, and a branchless cmovge picking the larger value into eax. The double version is pure Chapter 4: arguments in xmm0 and xmm1, a comisd setting the flags, one of the unsigned-style jumps (ja) that always follow floating-point compares, and a movsd register copy so the answer ends up in xmm0.
Look at what they share: nothing. Different register files, different compare instructions, different branching strategies, different return registers. One line of source produced both, but as machine code they are two unrelated listings that merely sit near each other. There is no such thing as "the assembly for maxOf", only the assembly for each instantiation.
The Name Tells You the Arguments
Those friendly labels are c++filt's work. The real symbols in the listing are mangled, and the mangling extends the _Z scheme from the constructors lesson to encode the template arguments:
_Z5maxOfIiET_S0_S0_ -> int maxOf<int>(int, int)
_Z5maxOfIdET_S0_S0_ -> double maxOf<double>(double, double)
You can read the important part directly: _Z5maxOf is the length-prefixed name, and the I...E bracket that follows lists the template arguments, Ii...E for <int>, Id...E for <double>. (The trailing T_S0_S0_ says the return type and both parameters reuse the template parameter, a compression scheme you never need to decode by hand.) As always, c++filt does the decoding for you. The practical payoff: because the arguments are baked into the symbol, you can scan a listing's labels and see exactly which instantiations of a template exist in the program.
No vtable in Sight
You have now seen the two ways C++ answers the question "which code runs for this type?", and they could not be more different. In the previous lesson, a virtual call was one function name resolved at run time: load the vtable pointer, load the slot, call rax. A template is many functions resolved at compile time: the call site in biggerInt is a plain direct call,
call int maxOf<int>(int, int)
with a fixed label the linker patches, exactly like every non-virtual call you have read. No hidden pointer in the object, no table in .rodata, no indirect call, no per-call cost of choosing. The choice was made when the compiler saw the argument types, and by the time the assembly exists the choice is gone; only its result remains. This is why templates are sometimes called static polymorphism: same flexibility in the source, but paid for at compile time in code generated rather than at run time in loads and indirect calls.
So the visual signatures to remember are: vtable loads followed by call rax means virtual dispatch; a direct call to a label with angle brackets in its demangled name means a template instantiation.
The Price Is Paid in Bytes
The cost of "one function per type" is exactly that: one function per type. For a three-instruction maxOf that is nothing. For a class template it multiplies quickly: std::vector<int> and std::vector<double> each instantiate their own push_back, their own growth logic, their own element copying, and they genuinely must, because the element sizes differ and the generated loads, stores, and address scaling differ with them. A program that uses many instantiations of many templates carries a full copy of the code for each one. This is code bloat: larger binaries and more pressure on the instruction cache.
Two things keep it in check. First, when several source files each instantiate maxOf<int>, every object file gets a copy, but they are marked so the linker keeps only one; you do not pay per translation unit. Second, some linkers can fold instantiations whose machine code happens to be byte-for-byte identical, such as vector<int*> and vector<double*>, since all pointers look alike at this level.
At -O2 They Usually Vanish
There is one more twist, and it previews the final chapter. Compile this at -O2:
int demo()
{
return maxOf(3, 7);
}
demo():
mov eax, 7
ret
maxOf<int> appears nowhere in the listing, not even as a label, if nothing else in the file needs it. The optimizer inlined the tiny instantiation into its caller and then folded the compare of two constants into the answer. This is the normal fate of small template functions at -O2: they inline and dissolve. When you go looking for a template in optimized assembly, you often cannot find the instantiation at all; you find only its effect, a comparison here, a constant there, woven into the caller. The function is not missing from the program's logic. It has simply stopped being a function.
Key Takeaways
- A template that is never instantiated produces no assembly at all; it is a recipe, not a function.
- Each instantiation is a separate, fully concrete function:
maxOf<int>usescmp/cmovon general-purpose registers,maxOf<double>usescomisdandxmmregisters, and they share nothing. - Mangled names encode the template arguments (
_Z5maxOfIiET_S0_S0_hasIi...Efor<int>);c++filtdecodes them, and the labels tell you which instantiations exist. - There is no run-time dispatch: calls to template functions are direct calls resolved at compile time, the opposite of the vtable's load-load-
call rax. - One copy of the code per distinct type argument is code bloat; the linker deduplicates identical instantiations across files, and can sometimes fold byte-identical ones.
- At
-O2small template functions usually inline and vanish; you find their effect in the caller, not the instantiation itself.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Templates in Assembly - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!