Limitations of Template Programming
The body of a template is the specification. To judge whether a type will work, read the body, list the operations it needs, and check that the type provides each one.
One Function for Many Types
Suppose you want one function that adds two values, whether they are integers, strings, or some type you wrote yourself. A C++ template lets you write that function once and have the compiler produce a version for each type it is used with. The practical question is how the compiler decides whether a particular type is acceptable for a template. This article answers that question and shows where the answer comes from.
The course source describes the rule as compile-time duck typing. Templates offer duck typing: a template argument is accepted based on the operations the template uses, not on a list of declared types. The check happens when the code is compiled, not while the program runs.
What do you think happens?
If a template is written to add two values with +, does the template need a list of approved types to accept an integer and a string?
Reveal answer
No, the template only needs the operations its body uses
Templates are accepted by duck typing. The header does not list allowed types. What matters is whether each operation the body uses is valid for the type that is supplied.
Reading a Template as Requirements
Read a template as a list of requirements rather than as a list of types. Consider a template that adds two values of type T. The parameter type T appears in the signature, but nothing in the body says which types T may be. The body simply assumes that whatever T is, the two arguments can be added together.
- template <typename T> declares T as a placeholder for a type supplied at the call site
- The body contains one expression, a + b, which names exactly one operation
- The call
add(2, 3)makes the compiler produce an int version of add - The call with std::string produces a string version of add
The body of a template is the specification. To judge whether a type will work, read the body, list the operations it needs, and check that the type provides each one.
Tracing the int and string Calls
Trace the two calls in main. For int, the expression a + b is valid, so the compiler produces an int version of add. For std::string, the expression a + b is also valid, because the string type provides concatenation through +, so the compiler produces a string version. Neither call required the template to know anything about the type's name. Only the operation mattered.
Deciding whether add accepts a type
Does the template add accept the call add(x, y) when x and y are both std::string?
- 1Identify the operation in the body
The body contains only a + b, so the single requirement is that + works on the two arguments.
- 2Substitute the type
With T set to std::string, the body becomes std::string + std::string.
- 3Check the operation
The string type provides concatenation through +, so the expression is valid.
- 4Decide
Every operation the body needs is available, so the compiler produces a string version of add.
The call is accepted, and the decision depended only on the operator +.
Following a Call Through the Body
The next case uses the same add template with a type the template cannot handle. The goal is not to memorize an error message but to see where the decision is made. Follow the argument from the call site, through the template body, to the point where the compiler must decide whether the body is valid for that argument.
The flowchart shows a single decision that is made for each argument type. The compiler never asks whether the type is on an approved list. It asks only whether the operation named in the body exists for that type. If it does, the version is produced. If it does not, the compile fails.
When Box Fails the Check
Now apply the same decision to a type called Box that the author wrote. For Box, the answer to the decision is no, because a + b is not defined for Box. The compiler therefore reports an error during compilation. Nothing about Box is wrong in itself. The template simply requires an operation that Box does not provide.
- Box is an ordinary struct with no operator+ defined
- The call
add(p, q)substitutes Box for T in the body - The compiler checks a + b for Box, finds no matching operation, and reports an error at compile time
Changing Box to provide operator+ would make the same call accepted. This shows that the acceptance rule depends on the operations available, not on a list of approved type names. The template header never changed, and neither did the call. Only the operations that Box provides changed.
Operations Decide, Not Declared Names
Duck typing gets its name from the saying that if something walks and quacks like a duck, it is treated as a duck. For templates, a type is treated as acceptable if it supports the operations the body uses. Compare this with a rule based on a declared list of types or on inheritance from a known base class. Those rules ask who the type is. Duck typing asks what the type can do.
| Question | Declared-type rule | Compile-time duck typing |
|---|---|---|
| What decides acceptance? | Whether the type appears on an approved list | Whether the body's operations are valid for the type |
| Where is the rule written? | In the template header or a separate list | In the template body |
| What does a new type need? | To be added to the list | The operations the body uses |
| Example for Box | Rejected unless listed | Rejected until operator+ is provided |
Two ways a template could decide whether a type is acceptable
Mistakes Beginners Make
Common mistake Assuming the template header lists the allowed types
What it looks likeReading template <typename T> and concluding that only the types named somewhere in the program can be used
Why it failsThe header only introduces the placeholder T. Nothing in the header says which types T may be.
Do this insteadRead the body and list the operations it needs, then check that the type provides each one.
Common mistake Assuming a type works because it worked for a different instantiation
What it looks likeSeeing add(2, 3) and add(std::string, std::string) compile, then expecting add(p, q) with two Box values to compile too
Why it failsEach instantiation is checked separately for the type that is substituted. A type that compiles in one call can fail in another if the body uses an operator the second type does not define.
Do this insteadCheck the operations the body uses against the specific type in each call.
Common mistake Assuming a missing operation only fails at runtime
What it looks likeExpecting the program to compile and then fail when add(p, q) runs
Why it failsThe compiler checks each instantiation when the template is compiled for a specific type, so an unsupported operation is reported as a compile-time error.
Do this insteadExpect the error at compile time and fix the missing operation, or choose a type that provides it.
Practice: Judge a New Type
The template below uses the same add function. Without running any code, decide whether a call add(p, q) with two values of a struct Pair compiles, where Pair has an int member and no operator+ defined. Then state which line of the template decides the answer.
Hints
- Ask which operation the body needs for the type T
- Check whether Pair provides that operation
- The decision happens at the expression a + b, not at the template header
Explain in one sentence why a type with no declared relationship to a list of approved types can still be accepted by a template.
Hints
- Think about what the acceptance rule depends on
- Compare the operations in the body with the operations the type provides
Practical Notes on Template Errors
When a template call fails, start from the template body rather than the header. List each operation the body performs on its parameters, then check the supplied type against that list. This narrows the search to the operation that is actually missing.
Key Takeaways
- A C++ 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 a type, read the template body, list the operations it needs, and check that the type provides each one.
- Each instantiation is checked when it is compiled for a specific type. An unsupported operation is reported as a compile-time error.
- A type can work in one call and fail in another if the body uses an operator the second type does not define.
- Providing the missing operation, such as operator+ for Box, makes the same call accepted without changing the template.