Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Customizing Template Behavior
Provide custom implementations for specific template argument types.
When the compiler instantiates a function template, it generates identical implementations for each type. This works well when all types need the same logic. However, sometimes specific types require fundamentally different behavior.
The limitation of generic templates
Consider a template that converts values to strings:
#include <iostream>
#include <string>
#include <sstream>
template <typename T>
std::string stringify(const T& value)
{
std::ostringstream out;
out << value;
return out.str();
}
int main()
{
std::cout << stringify(42) << '\n';
std::cout << stringify(3.14159) << '\n';
std::cout << stringify(true) << '\n';
return 0;
}
Output:
42
3.14159
1
The boolean prints as 1 instead of true. The generic implementation cannot distinguish between types that need different formatting.
Using a non-template function
The simplest solution is a regular (non-template) function for the specific type:
#include <iostream>
#include <string>
#include <sstream>
template <typename T>
std::string stringify(const T& value)
{
std::ostringstream out;
out << value;
return out.str();
}
std::string stringify(bool value)
{
return value ? "true" : "false";
}
int main()
{
std::cout << stringify(42) << '\n';
std::cout << stringify(3.14159) << '\n';
std::cout << stringify(true) << '\n';
return 0;
}
Output:
42
3.14159
true
When the compiler encounters stringify(true), it finds stringify(bool) and uses it directly instead of instantiating the template.
Non-template functions offer flexibility - they can have completely different signatures. The above example uses pass-by-value rather than matching the template's pass-by-const-reference.
Generally, prefer non-template functions for type-specific behavior.
Function template specialization
An alternative approach is explicit template specialization, which provides a custom implementation for specific types while remaining part of the template system:
#include <iostream>
#include <string>
#include <sstream>
#include <iomanip>
template <typename T>
std::string stringify(const T& value)
{
std::ostringstream out;
out << value;
return out.str();
}
template<>
std::string stringify<bool>(const bool& value)
{
return value ? "true" : "false";
}
template<>
std::string stringify<double>(const double& value)
{
std::ostringstream out;
out << std::fixed << std::setprecision(2) << value;
return out.str();
}
int main()
{
std::cout << stringify(42) << '\n'; // Primary template
std::cout << stringify(3.14159) << '\n'; // double specialization
std::cout << stringify(true) << '\n'; // bool specialization
return 0;
}
Output:
42
3.14
true
Specialization syntax explained
template<>
std::string stringify<double>(const double& value)
template<>indicates a specialization with no remaining template parametersstringify<double>identifies which type we are specializing- The parameter type must match what the primary template would use
Because all template parameters are specified, this is a full specialization.
Requirements for specialization
The primary template must be declared first
// Primary template declaration must come before specialization
template <typename T>
std::string convert(const T& value);
// Now we can specialize
template<>
std::string convert<char>(const char& value)
{
std::string result;
result += value;
return result;
}
The specialization signature must match
template <typename T>
void store(const T& item); // Primary uses const T&
// Correct - matches primary template signature
template<>
void store<int>(const int& item) { /* ... */ }
// Wrong - cannot change to pass-by-value
// template<>
// void store<int>(int item) { /* ... */ }
Specializations must be inline in headers
Full specializations are not implicitly inline. When defined in header files, mark them inline to prevent ODR violations:
// In header file
template<>
inline std::string stringify<bool>(const bool& value)
{
return value ? "true" : "false";
}
Precedence of function matching
When multiple matching functions exist, C++ applies this precedence:
- Non-template function (highest priority)
- Template specialization
- Primary template (lowest priority)
#include <iostream>
template <typename T>
void handle(const T& value)
{
std::cout << "Primary template: " << value << '\n';
}
template<>
void handle<int>(const int& value)
{
std::cout << "Specialization: " << value << '\n';
}
void handle(int value)
{
std::cout << "Non-template: " << value << '\n';
}
int main()
{
handle(42); // Non-template wins
handle(3.14); // Primary template
return 0;
}
Output:
Non-template: 42
Primary template: 3.14
Deleting specializations
Delete specializations to prevent template instantiation for specific types:
#include <iostream>
#include <cstring>
template <typename T>
T duplicate(T value)
{
return value;
}
// Prevent use with raw pointers
template<>
char* duplicate<char*>(char* value) = delete;
template<>
const char* duplicate<const char*>(const char* value) = delete;
int main()
{
int x{ duplicate(42) }; // OK
// const char* s{ duplicate("hello") }; // Error - deleted
return 0;
}
Deleted specializations generate compile-time errors instead of allowing potentially dangerous operations.
Drawbacks of function template specialization
Function template specialization has several issues:
-
Surprising overload resolution: Specializations do not participate in overload resolution the same way non-template functions do. The compiler picks the best template first, then checks for specializations of that template.
-
No partial specialization: Function templates can only be fully specialized. To specialize based on categories (e.g., all pointer types), you need class template specialization.
-
Maintenance burden: Specializations must exactly match the primary template signature. Refactoring the primary template requires updating all specializations.
Best practice: Use non-template overloads unless you specifically need template specialization behavior.
Member functions require class specialization
Consider a class template with a member function you want to specialize:
#include <iostream>
template <typename T>
class Logger
{
private:
T m_value{};
public:
Logger(T value)
: m_value{ value }
{
}
void log()
{
std::cout << m_value << '\n';
}
};
int main()
{
Logger<int> intLog{ 42 };
intLog.log();
Logger<bool> boolLog{ true };
boolLog.log(); // Prints 1, not "true"
return 0;
}
You might attempt to specialize just log():
// This doesn't compile
template<>
void Logger<bool>::log()
{
std::cout << (m_value ? "true" : "false") << '\n';
}
This fails because log() belongs to Logger<T>, not to a function template. When boolLog.log() is called, it invokes Logger<bool>::log() - a member of the Logger<bool> class instantiation.
To specialize member behavior, you need class template specialization, covered in the next lesson.
Summary
Function template specialization provides type-specific implementations while staying within the template system.
Use non-template overloads when:
- You need different parameter types or signatures
- You want simpler code
- The specialized version is fundamentally different
Use template specialization when:
- You must maintain an identical signature to the primary template
- You need to delete the template for specific types
- You need the specialization to be found through template argument deduction
Prefer non-template functions over template specialization when both approaches work.
Customizing Template Behavior - Quiz
Test your understanding of the lesson.
Practice Exercises
Specialized Printing for Different Types
Create a template function `print` that outputs values. Then specialize it for bool to print 'true'/'false' instead of 1/0, and for double to print with 2 decimal places.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!