Writing templatized code (C++)
C++ is, at its core, a language obsessed with two things: raw speed and less code.
I. Speed: Feed the Compiler, Reap the Performance
The compiler is not just a translator; it’s an optimizer waiting to be unleashed. The more you tell it upfront, the more ruthlessly it can act on your behalf.
This is the whole game behind constexpr and consteval: push computation out of runtime entirely. Behind noexcept, promise the stack won’t unwind, let the compiler breathe easier. Behind [[likely]], whisper to the branch predictor. Behind non-type template parameters, bake hard facts like matrix dimensions directly into the type, the way Eigen does with Matrix<float, 4, 4>. Behind _restrict_, swear that your pointers don’t alias, unlock a whole class of vectorization. And behind the deliberate spectrum from std::variant (compiler knows exactly what’s in the box), all the way down to std::any (compiler knows nothing, and you pay for that ignorance at runtime).
One hardcore example from the real world: many HFT traders don’t read config files at startup. They don’t load parameters from a database. They don’t keep types unknown for runtime. Instead, they maintain a types.h, a header that holds the day’s constants, thresholds, risk limits, alpha types, all hardcoded. Every morning, update the header, recompile. A fresh binary, with today’s reality burned directly into it. I like to call it types.h trick.
The principle is brutal in its simplicity: every fact you hide from the compiler is a performance tax you pay forever.
II. Less Code: Write Once, Instantiate Everywhere
The second obsession is eliminating repetition, not by copy-pasting, but by abstracting over types themselves. Templates let you write an algorithm once and let the compiler stamp out perfectly specialized versions for every type that needs it.
So, why do you need to write type-templatized code?
Say you’re building a class that works for a family of types G, but certain operations only make sense for subfamily S₁ ⊂ G, and others only for S₂ ⊂ G. The obvious fix, a separate class per type, quickly falls apart: code duplicates everywhere, and one change means touching every copy.
The way out is type templatization: instead of locking in concrete types upfront, you use abstract placeholders and let the compiler fill them in at instantiation time. Write the logic once; the compiler stamps out concrete versions as needed.
Can we control what types go in our type-templatized code?
Unconstrained templates let in the entire universe of C++ types, far too broad. You want to restrict the domain to exactly G. And the restriction doesn’t have to stop at type identity, you can go deeper and predicate on type traits: characteristics like whether a type is arithmetic, copyable, or satisfies a particular interface.
Even within G, you need selective behaviour: some code should kick in for S₁, some for S₂, never both. C++ gives you four mechanisms for this:
- Template Specialisation: provide an entirely separate implementation for a specific type or subfamily.
- SFINAE: goes a step further than specialisation; it can discriminate not just on type identity but on type traits. When a substitution is ill-formed, it quietly drops out of overload resolution rather than blowing up with a hard error.
- Concepts (C++20): the modern, readable replacement for SFINAE; same power, far less ceremony.
- if constexpr — within a single function body, branch at compile time based on subtype or type traits. The catch: substitution failures here are hard errors — there’s no graceful fallback, unlike SFINAE.
How to implement template specialization? How to restrict types in a template?
We know that templates can be specialized to differentiate code for different types. A generic template and its specialization look like this:
template<typename T, typename U>
class A{}
template<typename T>
class A<vector<T>, std::string> {}We can add whatever specialization we want, it can be type modifiers, type constructors, or exact type restrictions. Just the universe of types in the specialization needs to be a subset of what we have in the main template. There is one limitation to restriction, which is that specialization restriction is coarse-grained- it’s limited to categories of types, we can’t dig into specific types
Functions can’t be specialized like these, because we have overloading for them; we can do the exact same restriction, but we need to be in a function parameter.
How to implement SFINAE?
To prepare SFINAE, we need two ingredients: a type expression and a place where we can put it. A type expression can be built using different ways:
- real type expression
- simulation-based type expression where we use real-variables (decltype + declval),
- validity-based type expression: if a type expression is valid, then generate the given type expression; otherwise, remove this overload. we can use void_t or enable_if for this. Both are the same except void_t will always produce the void type and enable_if will produce give type.
These types of expressions are usually injected into a class/function signature. They can go in the function parameter or return type. We can also do a fake parameter for a function as well as a class template, where we specify a default value, so the user doesn’t have to pass any extra arguments.
We can also use SFINAE inside class member functions. In the figure, we have created a type trait is_class using pointer to member. This technique is slightly old-school and goes by a few names, sizeof-overload and sizeof-function-return type traits construction.
How compiler pick the best overload?
It’s like conducting an interview process; you first pick up candidates who have the same signature, name, and parameters. Once we have them, we do template substitution. Then remove the candidates that cause SFINAE failures. Finally, pick the most specialized one. Obviously, if multiple specialized, it will cause a compilation error.
What is typename = void? Why can’t detection idiom work if we did typename=int in primary template and void_t in specialization?
In templates, we don’t even need to give a default name to a template parameter that we don’t wanna use. So, typename T=void is the same as typename=void. It’s a phantom template parameter. Specialization is only recognized if it matches the primary template’s signature. Write typename = int on one side and typename = void on the other; they become strangers, neither specializing in the other. SFINAE has no home to return to.
Different types of type-traits? How are type-traits implemented?
What is the point of a variadic template?
Sometimes one type parameter isn’t the abstraction you need. The real question becomes: what if the thing you’re templating over is a list of types itself?
Variadic templates let you express exactly that, not “work for some type G,” but “work for an arbitrary constellation of types G1, G2, … Gn.” The parameter pack is the list; the template is a computation over it.
Get Tushar’s stories in your inbox
Join Medium for free to get updates from this writer.
The use cases diverge wildly depending on what you do with that list:
- Store one value of each type → std::tuple
- Fold a boolean predicate across all of them → std::conjunction
- Distill them down to a deduplicated set → type-list filtering
For std::tuple, if we didn’t have variadic templates, there would be infinite cases to implement.
partial specialization + recursion technique
Expanding type traits with variadic templates?
Some type-traits can be transformative, as we saw. Variadic templates can further expand the universe of type-traits. For instance, std::conjuction can combine multiple type-traits or type-expressions to give a single true/false value.
What are fold expressions? How are they connected with variadic templates?
Before fold expression, if we wanted to write a sum function over different types, it was like:
template<typename T>
T sum(T t){
return t;
}
template<typename T, typename ...Ts>
T sum(T t, Ts... ts){
return t+sum(ts...);
}This was a lot of boilerplate, writing base case, and all.
The fold expression introduced in C++17 simplified this problem. Now we can just calculate the sum using a fold expression as follows:
template<typename ...Ts>
T sum(Ts... ts){
return (... + ts); // left fold
}Limitations of fold expression compared to variadic template?
Fold expression can’t short-circuit. All components are computed, while variadic templates can short-circuit. For instance, std::conjunction short-circuits, but the corresponding fold expression can’t.
template<typename T, typename ...Ts>
class conjunction{
using type = std::conditional_t<std::is_same<T, false_type>,
false_type,
conjunction<Ts...>>; // this short circuits
}
template<typename ...Ts>
class conjunction{
using type = (... && Ts); // this doesn't short circuit
}Variadic template pack expansion rules?
If we have something like a pattern…, the pattern extends. If we don’t have a pattern, they are just declarations.
sizeof…(Ts) -> no pattern, number of types
typename… Ts -> no pattern, template declaration
sizeof(Ts),... -> pattern -> sizeof(int), sizeof(float), sizeof(bool)
cout << args << “ “, ... -> patternWhat is sizeof…. fold?
sizeof… is the simplest “fold”. sizeof…(Ts); gives count of types in pack
How to create concepts?
The usual syntax for concepts goes like this:
template<typename T>
concept MyConcept =
requires(T a, T b){
//expression validity check
typename T::value; // T should have value
a.function() // valid expression, T should have function()
{a.function()} // complex expression
{a.function()} -> std::integral // return type contrainst
{a.function()} noexcept // no except check
//boolean check
requires sizeof(T) > 8; // nested require check boolean true
requires std::is_move_constructible<T>Implement the std::tuple container and the get<i>() function?
With variadic templates, the technique is to always break the list of types called template peeling and do a recursive call.
template<typename T>
class tuple {
T t;
}
template<typename T, typename ... Ts>
class tuple: tuple<Ts...> {
T t;
}
template<size_t I, typename T, typename... Ts>
auto get(std::tuple<T, Ts...> t){
if constexpr(I==0)
return t.t;
else
return get<I-1>(static_cast<tuple<Ts...>&>(t))>; // upcast can be done statically
}Implement a zip container in which if the given list of input types is the same, then store them as an array; otherwise, store them as a tuple.
//APPROACH 1: specialize for array + SFINEA with void_t
template<typename ... Ts, typename = void>
class Zip {
std::tuple<Ts...> t;
}
template<typename T, typename...Ts>
class Zip<T, Ts..., std::void_t< std::conjunction<std::is_same<T, Ts>, ...> > > {
std::array<T, sizeof...(Ts) + 1> t;
}
//APPROACH 2: specialize + concepts
template<typename ... Ts>
class Zip {
std::tuple<Ts...> t;
}
template<typename T, typename...Ts>
class Zip<T, Ts...>
requires std::conjunction<std::is_same<T, Ts>, ...> { // concept boolean exp
std::array<T, sizeof...(Ts) + 1> t;
}Given template<typename… Ts> struct TypeSet, write a class that implements compile-time deduplication — i.e., TypeSet<int, float, int, bool, float> should be equivalent to TypeSet<int, float, bool>. Build a Contains-and-Append utility class.
template<typename ...Ts> struct TypeSet{};
template<typename T>
class MakeTypeSet{
using type = TypeSet<T>;
}
template<typename T, typename ...Ts>
class MakeTypeSet{
using type = std::conditional_t<Contains<T,Ts...>,
MakeTypeSet<Ts...>,
Append<T, MakeTypeSet<Ts...>>>;
}
template<typename T, typename T1, typename ...Ts>
class Contains{
using type = std::conditional_t<std::is_same<T, T1>, true_type, Contains<T, Ts...>>;
}
template<typename ...Ts>
class Append{
using type = TypeSet<Ts...>;
}
template<typename T, typename ...Ts> // specialize on TypeSet
class Append<T, TypeSet<Ts...> >{
using type = TypeSet<T, Ts...>;
}Note: std::conditional second and third arguments should be types like true_type and not boolean like true. std::conditional_t is the same as std::conditional::type
TypSet<> is an important container that can hold a list of types.
Implement std::apply. What is index_sequence, and how to construct it?
std::apply is a utility that takes in a function and a tuple and calls that function with that tuple’s values as arguments. index_sequence is exactly like TypeSet, but for a compile-time integer sequence. It’s mostly used to pass to a function, and the index sequence goes into a function template and can be used for a fold expression just like how variadic templates are used.
template<typename F, typename Tuple, size_t ...I>
auto apply_impl(F&f, Tuple& t, index_sequence<I...>){ // the function parameter is not useful
return f(std::get<I> > t, ...);
}
template<typename F, typename ...T>
auto apply(F& f, std::tuple<T...> t){
auto indseq = make_index_sequence<sizeof...(T)>();
return apply_impl(f, t, indseq);
}
template<size_t ...I>
class index_sequence{}
template<size_t N>
consteval auto make_index_sequence() {
if constexpr (N == 0)
return index_sequence<>{};
else
return Append<N>(make_index_sequence<N - 1>());
}
template<size_t N, size_t ...I>
consteval auto Append(index_sequence<I...>){
return index_sequence<I..., N>{};
}What is the difference between std::index_sequence and constexpr std::array? Both are fixed?
What is the future of generic programming in C++? What are Reflections (C++26)?
Reflection is the next leap in generic programming. The idea is simple but powerful: let the compiler expose what it knows about your types, at compile time, so your code can act on it generically.
With Reflection, you can inspect a class like an X-ray machine scans a bag. You see the structure, every member variable, every function, its name, its type, its signature, its access specifier. All of it, available as a compile-time list that you can iterate over, filter, and act on.
What you cannot see is what’s inside the function body, the actual logic, the syntax, the instructions. The X-ray shows that there’s a metal object in the bag. It does not tell you what the metal does. Same deal, Reflection sees that a function exists, what it takes, and what it returns. Not how it works inside. The following example illustrates that we don’t need individual print/debug logic for all the classes in code, immensely reducing boilerplate.
struct Person {
std::string name;
int age;
void greet() const;
};
template<typename T>
std::string debug(const T& obj)
{
std::string out;
out += type_name(^T);
out += "{ ";
bool first = true;
for constexpr (auto member : members_of(^T)){
if (!first)
out += ", ";
first = false;
out += name_of(member);
out += "=";
out += to_string(value_of(member, obj));
}
out += " }";
return out;
}In Action











