Template Compilation Basics
A C++ template lets you write the function once, and the compiler produces a version for each type the function is actually used with.
A Single Add Function for Many Types
Suppose you want one function that adds two values, whether they are integers, strings, or some type you wrote yourself. Writing a separate function for each type would repeat the same logic over and over. A C++ template lets you write the function once, and the compiler produces a version for each type the function is actually used with.
This article answers one question: how does the compiler decide whether a particular type is acceptable for a template? The course source calls the answer compile-time duck typing. Templates use duck typing, and the check happens when the code is compiled, not when the program runs.
What do you think happens?
Suppose a class Box has no addition operator defined. Will a call to the add template with two Box values compile?
Reveal answer
No. The body of add uses a + b, and that operation is not defined for Box, so the compiler reports an error during compilation.
The template header does not say which types are allowed. The only thing that decides acceptance is whether the operations used in the body are available for the type.
Reading the Template as a List of Requirements
The way to judge a template is to read its body as a list of requirements rather than as a list of types. Consider this template. Its body contains one expression, a + b, and that expression names exactly one operation.
- The parameter type T appears in the signature, but nothing in the body says which types T may be.
- The body assumes that whatever T is, a and b can be added together with +.
The signature mentions T, but the signature is not a list of allowed types. The only real requirement is hidden inside the body: the operation + must work on values of type T. Every type that the template is used with must provide that operation, and the compiler checks this for each type separately.
Tracing the Two Calls in main
Now call the template with two different types and follow what the compiler does for each. The example below uses int and std::string, and the same template body serves both.
5
concat- The first call instantiates add with T as int.
- The second call instantiates add with T as std::string.
Checking each call against the body
For each call to add, decide whether the template body is valid for the argument type.
- 1Call with int
The body expression a + b is valid for int, so the compiler produces an int version of add.
- 2Call with std::string
The string type provides concatenation through +, so the body expression a + b is valid, and the compiler produces a string version.
- 3What the name did not matter for
Neither call required the template to know anything about the type's name. Only the operation mattered.
Both calls are accepted because each type provides the single operation the body uses.
Where the Acceptance Decision Is Made
The decision about a type happens at the point where the compiler must check whether the body is valid for that argument. To judge a type in practice, read the template body, list the operations it needs, and check whether the type provides each one. Follow the argument from the call site, through the template body, to that check.
The flowchart shows the rule in one place. The type is not compared against a list of approved names. The compiler asks one question about the body: is each operation it uses defined for this type? If the answer is yes for every operation, the type is accepted. If any operation is missing, the compiler reports an error during compilation.
Tracing the Box Failure Step by Step
The next case uses the same add template with a type it cannot handle. The point is not to memorize an error message but to see where the decision is made. Here the type is a class named Box, and the body's a + b is not defined for Box. Nothing about Box is wrong in itself. The template simply requires an operation that Box does not provide.
Read the diagram from top to bottom. The caller's request is the start. The compiler instantiates the template for Box, and during that instantiation it checks the body's expression a + b against Box. That check fails, so the error is reported during compilation. The error is tied to the operation the body needs, which is why the fix belongs in Box, not in the call.
struct Box {
int value;
};
// add(Box{1}, Box{2}) fails to compile:
// no operator+ is defined for BoxBox has no operator+, so a + b in the body is not valid for Box and the call is rejected at compile time.
struct Box {
int value;
};
Box operator+(Box lhs, Box rhs) {
return Box{lhs.value + rhs.value};
}
// add(Box{1}, Box{2}) now compilesBox provides operator+, so the same call is accepted and the compiler produces a Box version of add.
Duck Typing Compared with Declared Types
An interface-based check would ask whether a type is declared as one of the allowed kinds, for example by inheriting from a base class. A template works differently. It accepts a type based on the operations its body uses. The table below contrasts the two ideas.
| Question | Declared-type acceptance | Template duck typing |
|---|---|---|
| What decides acceptance? | A list of declared types or base classes | The operations the template body uses |
| Where is the check made? | Against the declaration of the type | When the template is compiled for that specific type |
| What happens if a needed operation is missing? | Depends on the declared interface | A compile-time error is reported |
| Does the template header list allowed types? | Usually yes | No; the header does not say which types are allowed |
Declared-type acceptance compared with compile-time duck typing in templates.
Mistakes Beginners Make with Template Acceptance
Most mistakes come from two assumptions: that the template header lists the allowed types, and that a missing operation only fails at runtime. Both assumptions lead to the same error in reasoning, which is believing a type supports an operation when it does not.
Common mistake Assuming the template header lists the types the template accepts
What it looks likeReading template <typename T> T add(T a, T b) and concluding that only built-in numbers are allowed.
Why it failsThe header only introduces the type parameter. It does not enumerate types. Acceptance depends on what the body requires.
Do this insteadRead the body and list each operation it uses, such as a + b, then check whether the type provides each one.
Common mistake Assuming a type supports + because it looks like a number or a string
What it looks likePassing a custom Box value to add and expecting it to work because Box holds an int inside.
Why it failsBox holds an int, but Box itself does not define operator+. The body's a + b applies to Box values, not to the int inside them.
Do this insteadConfirm that the type itself provides the operation, or add the needed operator to the type, as in the Box example.
Common mistake Expecting a missing operation to be found only when the program runs
Why it failsThe source states that the compiler checks each instantiation when the template is compiled for a specific type, so the unsupported operation is reported as a compile-time error.
Do this insteadTreat a compile error from a template as the compiler telling you the type lacks a required operation, and fix the type or the call.
Checking a Type Before You Use It
A template function computes return a * b; for parameters a and b of type T. Without running any code, decide for each type whether the call should compile: int, std::string, and a class Box that defines no operator*. Then explain which step of the acceptance check decided each answer.
Hints
- Start by listing the single operation the body uses, which here is a * b.
- Ask whether the type itself provides that operation, not whether the type looks like a number or a string.
- For a type that fails, identify the point where the compiler reports the error.
Key Takeaways
- A template accepts a type based on the operations its body uses, not on a list of declared types. This is compile-time duck typing.
- To judge whether a type works with a template, read the body, list the operations it needs, and check that the type provides each one.
- The compiler checks each instantiation when the template is compiled for a specific type, so an unsupported operation is reported as a compile-time error.
- Errors come from assuming the template header lists allowed types or that a missing operation only fails at runtime.
- Fixing a rejected type means giving that type the needed operation, such as operator+ for Box, not changing the list of allowed names.