Home | Projects | Notes > C++ Programming > Generic Programming & Templates
"Writing code that works with a variety of types as arguments, as long as those argument types meet specific syntactic and semantic requirements." - Bjarne Stroustrup
Generic programming can be achieved by using:
Preprocessor macros - Be extra careful!
Templates
Function templates
Class templates
C++ preprocessor directives (#define).
No type information associated with the value.
Simple textual substitution.
Source file (.cpp)
7123
4if (num > MAX_SIZE)5 std::cout << "Too big";6
7double area = PI * r * r;Expanded source file (.i)
71// Removed2// Removed3
4if (num > 100)5 std::cout << "Too big";6
7double area = 3.14159 * r * r;All macros will be expanded by the preprocessor. (Textual substitution)
Making code more generic by using macro with arguments:
512
3std::cout << MAX(10, 20) << std::endl; // 204std::cout << MAX(2.4, 3.5) << std::endl; // 3.55std::cout << MAX('A', 'C') << std::endl; // CInstead of defining multiple functions per type:
31int max(int a, int b) { return (a > b) ? a : b; }2double max(double a, double b) { return (a > b) ? a : b; }3char max(char a, char b) { return (a > b) ? a : b; }Caution - Use parenthesis generously!
712
3result = SQUARE(5); // Expect 254result = 5 * 5; // Get 255
6result = 100 / SQUARE(5); // Expect 47result = 100 / 5 * 5; // Get 100Why this happens? It is because the preprocessor is doing the substitution not the compiler. The preprocessor does not know the syntax of the C++ prgoramming language.
Instead, guard each argument with parenthesis:
712
3result = SQUARE(5); // Expect 254result = ((5) * (5)); // Still get 255
6result = 100 / SQUARE(5); // Expect 47result = 100 / ((5) * (5)); // Now we get 4
A generic blueprint that the compiler uses to generate specialized functions and classes.
C++ supports function and class templates.
A template is defined with a placeholder type, allowing plugging-in any data type.
Compiler generates the appropriate function/class from the blue print at compile time. (The compiler performs type checking before the program executes.)
C++ supports the concept of generic programming / meta-programming. We provide a generic representation of a function or a class and then the compiler writes the actual function or class for us.
Revisiting the previous example, we can replace the type we want to generalize with a name, say T. But, now this won't compile.
11T max(T a, T b) { return (a > b) ? a : b; }We need to tell the compiler that this is a template function, and that T is the template parameter.
21template <typename T>2T max(T a, T b) { return (a > b) ? a : b; }Can use
classinstead oftypename.Note that this itself will not generate any code. It's simply a template or a bluprint. Code is not generated by the compiler until the compiler sees a specialized version of the template in the code.
Now the compiler can generate the appropriate function from the template. Note, this happens at compile-time.
41int a{10};2int b{20};3
4std::cout << max<int>(a, b);The syntax looks familiar to creating vectors and smart pointers, etc. You guessed it! They are all implemented as template classes.
Many times the compiler can deduce the type and the template parameter is not needed. Depending on the type of a and b, the compiler will figure it out.
21std::cout << max<double>(c, d);2std::cout << max(c, d);And we can use almost any type we need.
41char a{'A'};2char b{'B'};3
4std::cout << max(a, b) << std::endl;Notice that the type MUST support the > operator either natively or as an overloaded operator (operator>).
21template <typename T>2T max(T a, T b) { return (a > b) ? a : b; }The following will not compile unless Player overloads operator>.
41Player p1{"Hero", 100, 20};2Player p2{"Enemy", 99, 3};3
4std::cout << max<Player>(p1, p2);Also, you no longer have to worry about parentheses, unlike with macro functions, because this is handled by the compiler rather than the preprocessor.
11std::cout << max(5 + 2 * 2, 7 + 40);Templates can have multiple parameters, and their types can be different.
21template <typename T1, typename T2>2void func(T1 a, T2 b) { std::cout << a << " " << b; }When we use the function we provide the template parameters. often the compiler can deduce them.
21func<int, double>(10, 5.4); // 10 5.42func('A', 12.4); // A 12.4Although function templates can be powerful, you may need to overload the required operators to ensure they work correctly with your custom types.
The following class holds items where the item has a name and a data of any type.
xxxxxxxxxx1template <typename T>2class item3{4public:5 item(std::string name, T val) : name{name}, val{val} {}6 std::string get_name() const { return name; }7 T get_value() const { return val; }8private:9 std::string name;10 T val;11};12
13item<int> item1 {"Kyungjae", 1};14item<double> item2 {"House", 1000.0};15item<std::string> item3 {"Developer", "A"};16std::vecto<item<int>> v;17v.push_back(item<int>("Yena", 2));Just like function templates, class templates can also have multiple template parameters and their types can be different.
x
101template <typename T1, typename T2>2struct my_pair3{4 T1 first;5 T2 second;6};7
8my_pair<std::string, int> p1 {"Kyungjae", 100};9my_pair<int, double> p2 {124, 13.6};10std::vector<my_pair<int, double>> v;This is already defined in STL as std::pair.
xxxxxxxxxx12
3std::pair<std::string, int> p {"Kyungjae", 100};4std::cout << p.first; // Kyungjae5std::cout << p.second; // 100
This is just for practice purposes. Since C++11, the STL includes std::array, a template-based, fixed-size array class. Use std::array instead of raw arrays whenever possible for improved safety, usability, and integration with STL algorithms.
xxxxxxxxxx123
4// Here the 'N' is a non-type template parameter.5template <typename T, int N>6class array7{8public:9 array() = default;10 array(T init_val)11 {12 for (auto &v: values)13 v = init_val;14 }15 void fill(T val)16 {17 for (auto &v : values)18 v = val;19 }20 int get_size() const { return size; };21 T& operator[](int idx) { return values[idx]; }22 23private:24 int size {N};25 T values[N];26
27 friend std::ostream& operator<<(std::ostream &os, const array<T, N> &arr)28 {29 os << "[ ";30 for (const auto &v: arr.values)31 os << v << " ";32 os << "]" << std::endl;33 return os;34 }35};36
37int main(int argc, char *argv[])38{39 array<int, 5> a1;40 std::cout << "The size of a1 is: " << a1.get_size() << std::endl;41 std::cout << a1 << std::endl;42
43 a1.fill(0);44 std::cout << "The size of a1 is: " << a1.get_size() << std::endl;45 std::cout << a1 << std::endl;46
47 a1.fill(10);48 a1[0] = 1000; // a1.operator[](0)49 a1[3] = 2000;50 std::cout << a1 << std::endl;51
52 array<int, 100> a2 {1};53 std::cout << "The size of a2 is: " << a2.get_size() << std::endl;54 std::cout << a2 << std::endl;55
56 array<std::string, 10> strs(std::string{"ooo"});57 std::cout << "The size of strs is: " << strs.get_size() << std::endl;58 std::cout << strs << std::endl;59
60 strs[0] = std::string{"xxx"};61 std::cout << strs << std::endl;62
63 strs.fill(std::string{"X"});64 std::cout << strs << std::endl;65
66 return 0;67}All the array objects in the
mainfunction are created on the stack, not on the heap.