aboutsummaryrefslogtreecommitdiff
path: root/src/blog/2025-01-21-taming-cpp-templates/taming-cpp-templates.md
blob: 903a1ff6102bc750bf400c65022e13179b4e6706 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
# Taming C++, episode 3: templates, constraints and concepts

*This post is part of a [series](../../series)*

If you have ever written some C++, you have probably already used
[templates](https://en.cppreference.com/w/cpp/language/templates).
For example, you may have done something like this:

```
std::vector<int> v {1, 2, 3};
v[1] = -1;
std::cout << v[0] << ", " << v[1] << ", " << v[2] << std::endl;
```

The type
[`std::vector<int>`](https://en.cppreference.com/w/cpp/container/vector)
is the *specialization* of the *class template* `std::vector`. If you
wanted a vector of chars, for example, you could do something like this:

```
std::vector<char> w {'a', '!', '\n' };
```

Oversimplifying a bit, a template in C++ is a type (or a
[function](https://en.cppreference.com/w/cpp/language/function_template))
that depends on some other type or constant. They are quite powerful,
because they do a lot of work at compile time, so you can write generic
code without impacting performance. But at the same time they can be
quite intimidating, because as soon as you get something wrong around
templates the compiler will throw hundreds of lines of unreadable
error messages at you.

In this post I'll briefly explain what templates are and how to
write simple class and function templates. After that, I am going to
work through a little piece of code I wrote, starting from a simple
implementation and working out a general "templatized" version
step by step. I am going to use C++20 so that I can make use of
[concepts](https://en.cppreference.com/w/cpp/language/constraints),
so make sure to compile with `-std=c++20` if you are compiling with a
current version of GCC or Clang - in the future it may not be necessary
anymore, but currently both major compilers default to C++17.

You can find the code examples for this post in the
[companion repository for this series](https://git.tronto.net/taming-cpp),
and if you want you can also have a look at the final version
of my tiny library
[on my git page](https://git.tronto.net/zmodn). For
this post I am going to use Clang as a compiler, because I find its
error messages more readable most of the times.

## Templates

The first thing one must understand about templates is that **a
[class template](https://en.cppreference.com/w/cpp/language/class_template)
is not a class, it is a template for a class**.  Similarly, a function
template is not a function, it is a template for a function.

This means that a class template must be *specialized* to become an
actual class. The compiler won't generate any code for templates that
are not used anywhere. In other words, a template becomes something
concrete only when you provide concrete template arguments.

Different specializations of the same template are different types: one
cannot, for example, assign an `std::vector<int>` object to an
`std::vector<double>` variable.

### Class templates

As an example, let's take a simple standard library class such as
[`std::pair`](https://en.cppreference.com/w/cpp/utility/pair).
It could be implemented as follows (see
[pair.cpp](https://git.tronto.net/taming-cpp/tree/templates/pair.cpp)):

```
template<typename S, typename T>
class Pair {
public:
	S first;
	T second;

	Pair(S s, T t) : first{s}, second{t} {}

	void print() const { // Kinda useless, but I need it to explain a thing
		std::cout << "(" << first << ", " << second << ")";
	}
};
```

And then you can declare variables of type `Pair` - or rather, of a
specialization of `Pair`:

```
Pair<int, char> p(42, 'x');
```

The compiler can also deduce the template argument types for you, so
the statement above is equivalent to

```
Pair p(42, 'x');
```

So using templates is not that hard. Nice!

### Splitting declaration and implementation

Let's say you want to make our example class template `Pair` a bit cleaner
by splitting the declaration and the implementation of the `print()`
function. To do so, we have to re-declare the template parameters:

```
template<typename S, typename T>
class Pair {
public:
	S first;
	T second;

	Pair(S s, T t) : first{s}, second{t} {}

	void print() const;
};

template<typename S, typename T>
void Pair<S, T>::print() const {
	std::cout << "(" << first << ", " << second << ")";
}
```

The syntax is not amazing, but it can be worth it for longer functions.

### Function templates

Templates are not limited to classes, we can also have function templates.
A classic example is a
[swap](https://en.cppreference.com/w/cpp/algorithm/swap) function, which
can be implemented like this:

```
template<typename T>
void swap(T& a, T& b) {
	T tmp = a;
	a = b;
	b = tmp;
}
```

(Here I am using
[references](https://en.cppreference.com/w/cpp/language/reference),
which in case you don't know are just a simplified syntax for pointers.)

Let's take this simple example to show an important property of templates.
Let's say that, perhaps by mistake, we implemented the swap function
using different template types for `a` and `b` (see
[swap.cpp](https://git.tronto.net/taming-cpp/tree/templates/swap.cpp)):

```
template<typename S, typename T>
void swap(S& a, T& b) {
	S tmp = a;
	a = b;
	b = tmp;
}
```

This is actually ok, because **templates are not type-checked until
they are specialized**. And in fact we may legitimately want to
swap variables of different types, for example:

```
int x = 3;
double y = 1.0;
swap(x, y);
```

The code above will compile just fine, because C++ accepts implicit type
conversions between `int` and `double`. However, this:

```
int x = 3;
std::string y = "1.0";
swap(x, y);
```

Will not compile:

```
swap.cpp:6:6: error: assigning to 'int' from incompatible type 'std::basic_string<char>'
    6 |         a = b;
      |             ^
swap.cpp:13:2: note: in instantiation of function template specialization 'swap<int, std::basic_string<char>>' requested here
   13 |         swap(x, y);
      |         ^
1 error generated.
```

The error messages we get when misusing templates are not always nice
and readable as this one, they can literaly be hundreds of lines long.
Luckily, C++20 introduced
[constraints and concepts](https://en.cppreference.com/w/cpp/language/constraints)
to make these error messages more meaningful - we'll see some examples below.

### Non-type parameters

Objects, not just types, can be template parameters. A classic example
is [`std::array`](https://en.cppreference.com/w/cpp/container/array),
a fixed-size container where the capacity is fixed at compile time (see
[std_array.cpp](https://git.tronto.net/taming-cpp/tree/templates/std_array.cpp)
for an example).

Non-type parameters can be constants of any *structural type* - see
[this page](https://en.cppreference.com/w/cpp/language/template_parameters)
for a precise definition. Remember that you can only specialize them
with compile-time (i.e. `constexpr`) constants!

With non-type parameter you can do pretty wild stuff, see for example
[factorial.cpp](https://git.tronto.net/taming-cpp/tree/templates/factorial.cpp)
- although this specific example is not very useful, since it can easily
be replaced by a constexpr function.

Fun fact: if you use `auto`, you don't even have to specify a type for
a non-type parameter. For example, the following code works just fine (see
[println.cpp](https://git.tronto.net/taming-cpp/tree/templates/println.cpp)):

```
#include <iostream>

template<auto X> void println() { std::cout << X << std::endl; }

int main() {
	println<1.23>();
	println<42>();

	return 0;
}
```

Later we'll see a more useful application of this.

### Default values

Template parameters can have default value, for example:

```
template<typename S = int, typename T = S>
class Pair {
public:
	S first;
	T second;

	// And so on...
```

With the code above, `Pair` is going to denote a pair of two integers,
and `Pair<double>` is going to denote a pair of two doubles.

Non-type parameters can have default values too:

```
template<typename T, int N = 10>
class MyArray {
	// A container with 10 elements by default
};
```

### Variadic templates

Like with functions, templates can have a variable number of parameters.
A classic example is
[`std::tuple`](https://en.cppreference.com/w/cpp/utility/tuple), which
works similarly to `std::pair`, but accepts any number of items.

## Constraints and concepts

To explain the last features I want to talk about, I am going to use a
simlpe, albeit slightly unusual, example: let's implement a class
template for
[the integers modulo `N`](https://en.wikipedia.org/wiki/Modular_arithmetic),
where `N` is a fixed at compile-time.

We may start with something like this (see
[zmodn-1.cpp](https://git.tronto.net/taming-cpp/tree/templates/zmodn-1.cpp)):

```
#include <iostream>
#include <optional>
#include <tuple>

std::tuple<int, int, int> extended_gcd(int a, int b) {
	if (b == 0) return {a, 1, 0};
	auto [g, x, y] = extended_gcd(b, a%b);
	return {g, y, x - y*(a/b)};
}

template<int N>
class Zmod {
public:
	int value;

	Zmod(int z) : value{(z%N + N) % N} {}

	Zmod operator+(const Zmod& z) const { return value + z.value; }
	Zmod operator-(const Zmod& z) const { return value - z.value; }
	Zmod operator*(const Zmod& z) const { return value * z.value; }

	std::optional<Zmod> inverse() const {
		auto [g, a, _] = extended_gcd(value, N);
		return g == 1 ? Zmod(a) : std::optional<Zmod>{};
	}

	std::optional<Zmod> operator/(const Zmod& d) const {
		auto i = d.inverse();
		return i ? (*this) * i.value() : i;
	}

	std::optional<Zmod> operator/=(const Zmod& d) {
		auto q = *this / d;
		return q ? (*this = q.value()) : q;
	}
};

int main() {
	Zmod<57> x(34);
	Zmod<57> y(11);

	std::cout << "34 * 11 = " << (x * y).value << " (mod 57)" << std::endl;

	if (auto inv = y.inverse(); inv)
		std::cout << "11 * " << inv.value().value << " = 1 (mod 57)" << std::endl;
	else
		std::cout << "11 is not invertible in Z/57Z" << std::endl;

	return 0;
}
```

So we are just using a single non-type template parameter, no big deal.

But now let's say that by accident I type something like:

```
int main() {
	Zmod<0> z(13); // Oops, I meant Zmod<10>
}
```

Unfortunately, this code is going to compile just fine, and I'll get
a horrible run-time error. It would be cool if there was some way
to *constrain* the template argument to only allow positive integers.
Which brings us to...

### Constraints

[Constraints](https://en.cppreference.com/w/cpp/language/constraints)
are a way to prevent nasty run-time errors and / or make compiler errors
more meaningful when using templates; they were added in C++20.

In our case, introducing our constraint is quite simple (see
[zmodn-2.cpp](https://git.tronto.net/taming-cpp/tree/templates/zmodn-2.cpp)):

```
template<int N>
requires (N > 1)
class Zmod {
	// Same as before...
};
```

And now if we try to compile the `Zmod<0>` declaration we get:

```
zmodn-2.cpp:51:2: error: constraints not satisfied for class template 'Zmod' [with N = 0]
   51 |         Zmod<0> z(157);
      |         ^~~~~~~
zmodn-2.cpp:12:11: note: because '0 > 1' (0 > 1) evaluated to false
   12 | requires (N > 1)
      |           ^
1 error generated.
```

Nice!

### Making it more generic

For one specific application of this class, which I may write about in
a future post, I need `N` to be very larger, larger than a 32-bit
integer. So I should probably change `int` to `long long` or `int64_t`.
Except I would like to work with numbers that are even larger than 64
bits!  This means I should find (or write) a library for large integers,
but at the same time I want to keep the `Zmod` class independent of a
specific library... I should definitely make `Zmod` parametric in the
type of N.

In order to do so, I can use a non-type parameter declared `auto` and
[`decltype()`](https://en.cppreference.com/w/cpp/language/decltype) (see
[zmodn-3.cpp](https://git.tronto.net/taming-cpp/tree/templates/zmodn-3.cpp)):

```
template<auto N>
requires (N > 1)
class Zmod {
public:
	decltype(N) value;

	Zmod(decltype(N) z) : value{(z%N + N) % N} {}

	// The rest is unchanged
};
```

And of course I should also templatize the `extended_gcd()` function -
you can see the full code in
[zmodn-3.cpp](https://git.tronto.net/taming-cpp/tree/templates/zmodn-3.cpp).

Now we can use any type as a "base" for our modular integer! Well, almost.
I mentioned above that the type we use must be *structural*, but that is
relatively easy to satisfy. A bigger problem is that our type must
allow for compile-time constants - so we need, at least, a `constexpr`
constructor. I could not find a suitable library online, so I ended
up writing my own - see
[bigint.h](https://git.tronto.net/taming-cpp/tree/templates/bigint.h).

The code is simple and not very efficient, but this library is not meant
to be efficient. I am just using it for educational purposes.

To show it off, we can do stuff like this:

```
int main() {
	constexpr BigInt N("1000000000000000000000000000000");
	Zmod<N> x(BigInt("123456781234567812345678"));
	Zmod<N> y(BigInt("987654321987654321"));

	std::cout << x.value << " * "
	          << y.value << " (mod " << N << ") = "
	          << (x * y).value << std::endl;

	// Prints:
	// 123456781234567812345678 * 987654321987654321 (mod 1000000000000000000000000000000) = 5237873798636805364022374638

	return 0;
}
```

But now we have once again a constraint problem. We could for example write

```
	constexpr double M = 3.14;
	Zmod<M> z(M);
```

And sure, this will fail with an understandable error message related to
the modulo operation `%`, but it is not hard to imagine that in other
situations this could be a problem. So we should put a constraint on
our type, in this case `decltype(N)`.

At first I achieved this using the *type trait*
[`std::is_integral`](https://en.cppreference.com/w/cpp/types/is_integral):

```
template<auto N>
requires (N > 1) && std::is_integral<decltype(N)>::value
class Zmod {
	// Etc...
};
```

Type traits are defined in the `<type_traits>` header. For an overview
check out this nice
[blog post](https://www.internalpointers.com/post/quick-primer-type-traits-modern-cpp).

Unfortunately, my custom big integer class does not satisfy
`std::is_integral`.  So I have to define my own set of constraints.

### Concepts

Along with constraints, C++20 also introduced the possibility to define
and name a custom set of requirements. This can be done with *concepts*.
In our example, we can require that our type supports all the operations
we need (see
[zmodn-4.cpp](https://git.tronto.net/taming-cpp/tree/templates/zmodn-4.cpp)):

```
template<typename T>
concept Integer = requires(T a, T b, int i) {
	{T(i)};

	{a + b} -> std::same_as<T>;
	{a - b} -> std::same_as<T>;
	{a * b} -> std::same_as<T>;
	{a / b} -> std::same_as<T>;
	{a % b} -> std::same_as<T>;

	{a == b} -> std::same_as<bool>;
	{a != b} -> std::same_as<bool>;
};
```

Let's break this down. The first line introduces a template, because our
concept depends on a type parameter `T`. The second line introduces the
definition of the concept: the arguments to the `requires` keyword are
variables that we are going to use in our concept definition.

The third line is where things get interesting. The notation `{T(i)}`
means "`T(i)` must be a valid expression", where `i` is any variable of
type `int`, as defined in the `requires` arguments. In other words, we
are asking that type `T` has a constructor that takes a single integer
parameter.

The other lines are all similar, and they expand on this concept.
For example `{a % b} -> std::same_as<T>` requires that the operator `%` is
defined between variables of type T; moreover, the `-> std::same_as<T>`
notation declares that we want the resulting type to satisfy the
type trait `std::same_as<T>` - in other words, we are asking for
`T operator%(T)` to be defined as a member function of `T`.

Note that we are not requiring anything about what these operations
actually do: we are only requiring that they are defined. If for some
reason a custom floating point type defines an operator `%` and all other
arithmetic operations that we require, it could be used for our `zmod<N>`
class without any complaints from the compiler, but the results may not
be what we expect.

We can use our newly-defined concept in two ways. As part of a requires
clause:

```
template<typename T>
requires Integer<T>
std::tuple<T, T, T> extended_gcd(T a, T b) { /* Same as before */ }

template<auto N>
requires (N > 1) && Integer<decltype(N)>
class Zmod { /* Same as before */ }
```

Or with the following syntax sugar:

```
template<Integer T>
std::tuple<T, T, T> extended_gcd(T a, T b) { /* Same as before */ }

template<Integer auto N>
requires(N > 1)
class Zmod { /* Same as before */ }
```

I tend to prefer the second way because it is more compact and it reads
nicely: in the first of the two templates, we declare `T` as if it were
a variable of type `Integer`.

And now if we try to compile `Zmod<3.14>` the compiler gives us a clear
error message:

```
zmodn-4.cpp:68:3: error: constraints not satisfied for class template 'Zmod' [with N = 3.140000e+00]
   68 |          Zmod<M> z(4);
      |          ^~~~~~~
zmodn-4.cpp:29:10: note: because 'decltype(3.1400000000000001)' (aka 'double') does not satisfy 'Integer'
   29 | template<Integer auto N>
      |          ^
zmodn-4.cpp:16:5: note: because 'a % b' would be invalid: invalid operands to binary expression ('double' and 'double')
   16 |         {a % b} -> std::same_as<T>;
      |            ^
1 error generated.
```

## Conclusion

Templates are a very powerful tool that allow creating zero-cost
abstractions (if you don't count the longer compile time as a cost).
With the addition of constraints and concepts in C++20 it became easier to
define requirements on the template parameters and catch template misuse
at compile time.

In some way concepts offer a new style of abstraction, similar in scope
to object-oriented programming. Since I am not a fan of OOP, I like that
we have this new option, and I am going to play around with whenever I
get the chance.

Generated with cgit - Back to sebastiano.tronto.net