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)
71
2
3
4if (num > MAX_SIZE)
5 std::cout << "Too big";
6
7double area = PI * r * r;
Expanded source file (.i)
71// Removed
2// Removed
3
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:
51
2
3std::cout << MAX(10, 20) << std::endl; // 20
4std::cout << MAX(2.4, 3.5) << std::endl; // 3.5
5std::cout << MAX('A', 'C') << std::endl; // C
Instead 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!
71
2
3result = SQUARE(5); // Expect 25
4result = 5 * 5; // Get 25
5
6result = 100 / SQUARE(5); // Expect 4
7result = 100 / 5 * 5; // Get 100
Why 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:
71
2
3result = SQUARE(5); // Expect 25
4result = ((5) * (5)); // Still get 25
5
6result = 100 / SQUARE(5); // Expect 4
7result = 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
class
instead 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.4
2func('A', 12.4); // A 12.4
Although 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.
xxxxxxxxxx
1template <typename T>
2class item
3{
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_pair
3{
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
.
xxxxxxxxxx
1
2
3std::pair<std::string, int> p {"Kyungjae", 100};
4std::cout << p.first; // Kyungjae
5std::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.
xxxxxxxxxx
1
2
3
4// Here the 'N' is a non-type template parameter.
5template <typename T, int N>
6class array
7{
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
main
function are created on the stack, not on the heap.