C++ Template Specialization and Partial Specialization
Templates are the core mechanism of C++ generic programming, while template specialization and partial specialization provide finer control over templates, allowing customized implementations for specific types.
Template Basics
Function template:
cpptemplate <typename T> T max(T a, T b) { return (a > b) ? a : b; } // Usage int result = max(10, 20); // T = int double result2 = max(3.14, 2.71); // T = double
Class template:
cpptemplate <typename T> class Stack { private: std::vector<T> elements; public: void push(const T& element) { elements.push_back(element); } T pop() { T element = elements.back(); elements.pop_back(); return element; } }; // Usage Stack<int> intStack; Stack<std::string> stringStack;
Template Specialization
Full Specialization: Provide concrete types for all template parameters, completely replacing the template definition.
cpp// Generic template template <typename T> class Vector { public: void push(const T& value) { std::cout << "Generic push: " << value << std::endl; } }; // Full specialization for bool type template <> class Vector<bool> { public: void push(bool value) { std::cout << "Bool push: " << (value ? "true" : "false") << std::endl; } }; // Usage Vector<int> intVec; intVec.push(42); // Output: Generic push: 42 Vector<bool> boolVec; boolVec.push(true); // Output: Bool push: true
Function template specialization:
cpp// Generic template template <typename T> bool compare(T a, T b) { std::cout << "Generic compare" << std::endl; return a < b; } // Specialization for const char* template <> bool compare<const char*>(const char* a, const char* b) { std::cout << "String compare" << std::endl; return strcmp(a, b) < 0; } // Usage compare(10, 20); // Output: Generic compare compare("hello", "world"); // Output: String compare
Template Partial Specialization
Partial specialization only applies to class templates, allowing partial specification of template parameters.
Basic example:
cpp// Generic template: two type parameters template <typename T, typename U> class Pair { public: T first; U second; void print() { std::cout << "Pair<" << typeid(T).name() << ", " << typeid(U).name() << ">" << std::endl; } }; // Partial specialization: same type for both parameters template <typename T> class Pair<T, T> { public: T first; T second; void print() { std::cout << "Pair<" << typeid(T).name() << ", " << typeid(T).name() << "> (Same types)" << std::endl; } }; // Partial specialization: second parameter is a pointer template <typename T> class Pair<T, T*> { public: T first; T* second; void print() { std::cout << "Pair<" << typeid(T).name() << ", " << typeid(T).name() << "*> (Pointer)" << std::endl; } }; // Usage Pair<int, double> p1; // Uses generic template p1.print(); // Pair<int, double> Pair<int, int> p2; // Uses partial specialization (same types) p2.print(); // Pair<int, int> (Same types) Pair<int, int*> p3; // Uses partial specialization (pointer) p3.print(); // Pair<int, int*> (Pointer)
Pointer and Reference Partial Specialization
cpp// Generic template template <typename T> class TypeInfo { public: static const char* name() { return "Unknown type"; } }; // Partial specialization for pointer types template <typename T> class TypeInfo<T*> { public: static const char* name() { return "Pointer type"; } }; // Partial specialization for reference types template <typename T> class TypeInfo<T&> { public: static const char* name() { return "Reference type"; } }; // Partial specialization for const types template <typename T> class TypeInfo<const T> { public: static const char* name() { return "Const type"; } }; // Usage std::cout << TypeInfo<int>::name() << std::endl; // Unknown type std::cout << TypeInfo<int*>::name() << std::endl; // Pointer type std::cout << TypeInfo<int&>::name() << std::endl; // Reference type std::cout << TypeInfo<const int>::name() << std::endl; // Const type
SFINAE (Substitution Failure Is Not An Error)
SFINAE is an important technique in template metaprogramming, allowing templates to be excluded when template parameter substitution fails, rather than producing compilation errors.
Basic example:
cpp// Check if type has value_type member template <typename T> class has_value_type { template <typename U> static auto test(int) -> decltype(typename U::value_type(), std::true_type{}); template <typename> static std::false_type test(...); public: static constexpr bool value = decltype(test<T>(0))::value; }; // Function overloading using SFINAE template <typename T> typename std::enable_if<has_value_type<T>::value, void>::type process(T container) { std::cout << "Container has value_type" << std::endl; } template <typename T> typename std::enable_if<!has_value_type<T>::value, void>::type process(T value) { std::cout << "Type doesn't have value_type" << std::endl; } // Usage std::vector<int> vec; process(vec); // Container has value_type int x = 42; process(x); // Type doesn't have value_type
C++17 if constexpr:
cpptemplate <typename T> void printTypeInfo(T value) { if constexpr (std::is_pointer_v<T>) { std::cout << "Pointer type" << std::endl; } else if constexpr (std::is_integral_v<T>) { std::cout << "Integral type" << std::endl; } else if constexpr (std::is_floating_point_v<T>) { std::cout << "Floating point type" << std::endl; } else { std::cout << "Other type" << std::endl; } } // Usage int* ptr = nullptr; printTypeInfo(ptr); // Pointer type int num = 42; printTypeInfo(num); // Integral type double d = 3.14; printTypeInfo(d); // Floating point type
Template Metaprogramming
Compile-time computation:
cpp// Compile-time factorial calculation template <int N> struct Factorial { static constexpr int value = N * Factorial<N - 1>::value; }; // Specialization as recursion termination condition template <> struct Factorial<0> { static constexpr int value = 1; }; // Usage constexpr int result = Factorial<5>::value; // 120
Compile-time type checking:
cpptemplate <typename T> struct IsPointer { static constexpr bool value = false; }; template <typename T> struct IsPointer<T*> { static constexpr bool value = true; }; // Usage static_assert(IsPointer<int*>::value == true); static_assert(IsPointer<int>::value == false);
Type Traits
C++11 introduced the <type_traits> header, providing rich type trait tools.
Common type traits:
cpp#include <type_traits> // Check type properties static_assert(std::is_integral_v<int> == true); static_assert(std::is_floating_point_v<double> == true); static_assert(std::is_pointer_v<int*> == true); static_assert(std::is_reference_v<int&> == true); static_assert(std::is_const_v<const int> == true); // Type transformations using IntPtr = std::add_pointer_t<int>; // int* using ConstInt = std::add_const_t<int>; // const int using RemoveConst = std::remove_const_t<const int>; // int // Conditional types template <typename T> using ElementType = typename std::conditional< std::is_pointer_v<T>, std::remove_pointer_t<T>, T >::type; // Usage static_assert(std::is_same_v<ElementType<int*>, int>); static_assert(std::is_same_v<ElementType<int>, int>);
Practical Application Examples
Smart pointer deleter specialization:
cpptemplate <typename T> class SmartPtr { private: T* ptr; public: explicit SmartPtr(T* p = nullptr) : ptr(p) {} ~SmartPtr() { delete ptr; } }; // Specialization for array types template <typename T> class SmartPtr<T[]> { private: T* ptr; public: explicit SmartPtr(T* p = nullptr) : ptr(p) {} ~SmartPtr() { delete[] ptr; } }; // Usage SmartPtr<int> ptr1(new int(42)); SmartPtr<int[]> ptr2(new int[10]);
Container optimization specialization:
cpptemplate <typename T, size_t N> class FixedArray { private: T data[N]; public: T& operator[](size_t index) { return data[index]; } }; // Specialization for bool type, using bit compression template <size_t N> class FixedArray<bool, N> { private: unsigned char data[(N + 7) / 8]; public: bool operator[](size_t index) { return (data[index / 8] >> (index % 8)) & 1; } };
Best Practices
1. Prefer type traits over manual specialization
cpp// Recommended template <typename T> void process(T value) { if constexpr (std::is_pointer_v<T>) { // Handle pointer } else { // Handle non-pointer } } // Not recommended template <typename T> void process(T value); template <typename T> void process(T* value);
2. Use constexpr for compile-time computation
cpp// C++11/14 template <int N> struct Factorial { static constexpr int value = N * Factorial<N - 1>::value; }; // C++17 constexpr int factorial(int n) { if (n <= 1) return 1; return n * factorial(n - 1); }
3. Use concepts (C++20) to constrain templates
cpptemplate <typename T> concept Integral = std::is_integral_v<T>; template <Integral T> T add(T a, T b) { return a + b; } // Usage add(10, 20); // OK add(3.14, 2.71); // Compilation error
4. Avoid overly complex template metaprogramming
cpp// Not recommended: overly complex template <typename T> struct ComplexMetaProgramming { // Lots of nested templates }; // Recommended: simple and clear template <typename T> void simpleFunction(T value) { // Simple, direct implementation }
Notes
- Function templates don't support partial specialization, only full specialization
- Specialized versions must be declared after the primary template
- Specialized versions must have the same interface as the primary template
- SFINAE can lead to difficult-to-understand compilation error messages
- Overuse of template specialization can lead to code bloat
- C++20 concepts can replace some SFINAE usage, providing better readability