Advanced C++ Template Techniques
Non-Type Template Parameters
Template parameters are categorized into type parameters and non-type parameters. A type parameter is preceded by class or typename in the template list, while a non-type parameter is a compile-time constant that functions as a parameter for class or function templates and is treated as a constant within them.
namespace cpp_plus_plus {
// Static array template with default size
template<typename ElemType, unsigned int DefaultSize = 10>
class FixedArray {
public:
ElemType& At(unsigned int idx) { return arr[idx]; }
const ElemType& At(unsigned int idx) const { return arr[idx]; }
unsigned int Count() const { return length; }
bool IsEmpty() const { return length == 0; }
private:
ElemType arr[DefaultSize];
unsigned int length = 0;
};
}
#include <iostream>
using namespace std;
template<typename ValueType, unsigned int FixedVal = 10>
void DisplayFixedValue(ValueType& val) {
val = FixedVal;
cout << val << endl;
}
int main() {
int x = 3;
DisplayFixedValue(x);
return 0;
}
Key Points:
- Floating-point numbers, class objects, and strings cannot serve as non-type template parameters.
- Non-type parameters must be determinable at compile time.
Template Specialization
Function Template Specialization
A specialized function template overrides the default template behavior for specific types. Steps to specialize a function template:
- Define a base function template first.
- Use
template<>followed by the specialized functon name. - Specify the target type(s) in angle brackets after the function name.
- Match the base template's parameter list exactly to avoid errors.
#include <iostream>
using namespace std;
struct OrderDate {
int year, month, day;
OrderDate(int y, int m, int d) : year(y), month(m), day(d) {}
bool operator<(const OrderDate& rhs) const {
return year < rhs.year || (year == rhs.year && month < rhs.month) || (year == rhs.month && month == rhs.month && day < rhs.day);
}
};
// Base function template for comparison
template<typename CompType>
bool CompareElements(CompType left, CompType right) {
return left < right;
}
// Specialization for OrderDate pointers
template<>
bool CompareElements<OrderDate*>(OrderDate* left, OrderDate* right) {
return *left < *right;
}
int main() {
cout << CompareElements(1, 2) << endl;
OrderDate d1(2022, 7, 7), d2(2022, 7, 8);
cout << CompareElements(d1, d2) << endl;
OrderDate* p1 = &d1, * p2 = &d2;
cout << CompareElements(p1, p2) << endl;
return 0;
}
Note: For simple type-specific fixes, defining a separate non-template function is often easier than specializing.
Class Template Specialization
Full Specialization
Full specialization replaces all template parameters with specific types.
#include <iostream>
using namespace std;
template<typename TypeA, typename TypeB>
class PairStorage {
public:
PairStorage() { cout << "PairStorage<TypeA, TypeB>" << endl; }
private:
TypeA item1;
TypeB item2;
};
// Full specialization for TypeA=int, TypeB=char
template<>
class PairStorage<int, char> {
public:
PairStorage() { cout << "PairStorage<int, char>" << endl; }
private:
int item1;
char item2;
};
void TestSpecializations() {
PairStorage<int, int> s1;
PairStorage<int, char> s2;
}
Partial Specialization
Partial specialization restricts or fixes part of the template parameters. It can also impose tighter constraints on parameters.
Fixing a Subset of Parameters
// Specialization for TypeB=double
template<typename TypeA>
class PairStorage<TypeA, double> {
public:
PairStorage() { cout << "PairStorage<TypeA, double>" << endl; }
private:
TypeA item1;
double item2;
};
Imposing Tighter Constraints
// Specialization for both parameters as references
template<typename TypeA, typename TypeB>
class PairStorage<TypeA&, TypeB&> {
public:
PairStorage(const TypeA& a, const TypeB& b) : item1(a), item2(b) {
cout << "PairStorage<TypeA&, TypeB&>" << endl;
}
private:
const TypeA& item1;
const TypeB& item2;
};
// Specialization for both parameters as pointers
template<typename TypeA, typename TypeB>
class PairStorage<TypeA*, TypeB*> {
public:
PairStorage() { cout << "PairStorage<TypeA*, TypeB*>" << endl; }
private:
TypeA* item1;
TypeB* item2;
};
Usage Example:
void TestPartialSpecializations() {
PairStorage<double, int> s3;
PairStorage<int, double> s4;
PairStorage<int*, char*> s5;
int a = 10, b = 20;
PairStorage<int&, int&> s6(a, b);
}
Template Separate Compilation
What Is Separate Compilation?
Separate compilation splits a program into multiple source files, compiling each to an object file and linking them into an executable.
Issues with Templates and Separation
Templates are instantiated on demand. If a template's declaration and definition are split between header (.h) and implementation (.cpp) files:
- The .cpp file's compiler lacks instantiation context, so no function code is generated.
- The .h file's compiler has a declaration but no definition for instantiation.
- The linker fails to find the missing functon definitions.
Example Files:
FixedArray.h(declaration)
#include <iostream>
#include <cassert>
namespace cpp_plus_plus {
template<typename ElemType, unsigned int ArrSize = 10>
class FixedArray {
public:
unsigned int Count() const;
private:
ElemType arr[ArrSize];
unsigned int length = 0;
};
void HelperFunc();
}
FixedArray.cpp(definition)
namespace cpp_plus_plus {
template<typename ElemType, unsigned int ArrSize>
unsigned int FixedArray<ElemType, ArrSize>::Count() const {
return length;
}
void HelperFunc() {
std::cout << "cpp_plus_plus::HelperFunc()" << std::endl;
}
// Explicit instantiation
template class FixedArray<int>;
template class FixedArray<double>;
}
main.cpp(usage)
#include "FixedArray.h"
using namespace cpp_plus_plus;
int main() {
FixedArray<int> arr1;
std::cout << arr1.Count() << std::endl;
HelperFunc();
FixedArray<double> arr2;
std::cout << arr2.Count() << std::endl;
FixedArray<std::string> arr3;
// std::cout << arr3.Count() << std::endl; // Error: No explicit instantiation for std::string
return 0;
}
Solutions
- Combine Declaration and Definition: Place all template code (declaration + definition) in a single header file (or
.hppfile). This allows the compiler to instantiate the template when it processes the header. - Explicit Instantiation: List all required instantiation types in the
.cppfile (as in theFixedArray.cppexample). This is inflexible for generic use.
Template Summary
Advantages:
- Code Reuse: Eliminates redundant code for similar data types.
- Flexibility: Enables writing generic algorithms that work with any compatiblle type.
Disadvantages:
- Code Bloat: Each unique instantiation generates separate binary code.
- Debugging Challenges: Compilation errors for templates are often verbose and hard to interpret.