aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/blog/2024-12-26-taming-cpp-raii/taming-cpp-raii.md2
-rw-r--r--src/blog/2025-01-21-taming-cpp-templates/taming-cpp-templates.md578
-rw-r--r--src/series/series.md1
3 files changed, 581 insertions, 0 deletions
diff --git a/src/blog/2024-12-26-taming-cpp-raii/taming-cpp-raii.md b/src/blog/2024-12-26-taming-cpp-raii/taming-cpp-raii.md
index 88744bb..f35596f 100644
--- a/src/blog/2024-12-26-taming-cpp-raii/taming-cpp-raii.md
+++ b/src/blog/2024-12-26-taming-cpp-raii/taming-cpp-raii.md
@@ -278,3 +278,5 @@ other object-oriented languages like Java or C#. C++ gives you a lot
278of control over the low-level details, and some powerful tools to make 278of control over the low-level details, and some powerful tools to make
279use of it, in exchange for a lot of complexity that you must, at the 279use of it, in exchange for a lot of complexity that you must, at the
280very least, be aware of. 280very least, be aware of.
281
282*Next in the series: [RAII](../2025-01-21-taming-cpp-templates)*
diff --git a/src/blog/2025-01-21-taming-cpp-templates/taming-cpp-templates.md b/src/blog/2025-01-21-taming-cpp-templates/taming-cpp-templates.md
new file mode 100644
index 0000000..fe1ca5d
--- /dev/null
+++ b/src/blog/2025-01-21-taming-cpp-templates/taming-cpp-templates.md
@@ -0,0 +1,578 @@
1# Taming C++, episode 3: templates, constraints and concepts
2
3*This post is part of a [series](../../series)*
4
5If you have ever written some C++, you have probably already used
6[templates](https://en.cppreference.com/w/cpp/language/templates).
7For example, you may have done something like this:
8
9```
10std::vector<int> v {1, 2, 3};
11v[1] = -1;
12std::cout << v[0] << ", " << v[1] << ", " << v[2] << std::endl;
13```
14
15The type
16[`std::vector<int>`](https://en.cppreference.com/w/cpp/container/vector)
17is the *specialization* of the *class template* `std::vector`. If you
18wanted a vector of chars, for example, you could do something like this:
19
20```
21std::vector<char> w {'a', '!', '\n' };
22```
23
24Oversimplifying a bit, a template in C++ is a type (or a
25[function](https://en.cppreference.com/w/cpp/language/function_template))
26that depends on some other type or constant. They are quite powerful,
27because they do a lot of work at compile time, so you can write generic
28code without impacting performance. But at the same time they can be
29quite intimidating, because as soon as you get something wrong around
30templates the compiler will throw hundreds of lines of unreadable
31error messages at you.
32
33In this post I'll briefly explain what templates are and how to
34write simple class and function templates. After that, I am going to
35work through a little piece of code I wrote, starting from a simple
36implementation and working out a general "templatized" version
37step by step. I am going to use C++20 so that I can make use of
38[concepts](https://en.cppreference.com/w/cpp/language/constraints),
39so make sure to compile with `-std=c++20` if you are compiling with a
40current version of GCC or Clang - in the future it may not be necessary
41anymore, but currently both major compilers default to C++17.
42
43You can find the code examples for this post in the
44[companion repository for this series](https://git.tronto.net/taming-cpp),
45and if you want you can also have a look at the final version
46of my tiny library
47[on my git page](https://git.tronto.net/zmodn/file/README.md.html). For
48this post I am going to use Clang as a compiler, because I find its
49error messages more readable most of the times.
50
51## Templates
52
53The first thing one must understand about templates is that **a
54[class template](https://en.cppreference.com/w/cpp/language/class_template)
55is not a class, it is a template for a class**. Similarly, a function
56template is not a function, it is a template for a function.
57
58This means that a class template must be *specialized* to become an
59actual class. The compiler won't generate any code for templates that
60are not used anywhere. In other words, a template becomes something
61concrete only when you provide concrete template arguments.
62
63Different specializations of the same template are different types: one
64cannot, for example, assign an `std::vector<int>` object to an
65`std::vector<double>` variable.
66
67### Class templates
68
69As an example, let's take a simple standard library class such as
70[`std::pair`](https://en.cppreference.com/w/cpp/utility/pair).
71It could be implemented as follows (see
72[pair.cpp](https://git.tronto.net/taming-cpp/file/templates/pair.cpp.html)):
73
74```
75template<typename S, typename T>
76class Pair {
77public:
78 S first;
79 T second;
80
81 Pair(S s, T t) : first{s}, second{t} {}
82
83 void print() const { // Kinda useless, but I need it to explain a thing
84 std::cout << "(" << first << ", " << second << ")";
85 }
86};
87```
88
89And then you can declare variables of type `Pair` - or rather, of a
90specialization of `Pair`:
91
92```
93Pair<int, char> p(42, 'x');
94```
95
96The compiler can also deduce the template argument types for you, so
97the statement above is equivalent to
98
99```
100Pair p(42, 'x');
101```
102
103So using templates is not that hard. Nice!
104
105### Splitting declaration and implementation
106
107Let's say you want to make our example class template `Pair` a bit cleaner
108by splitting the declaration and the implementation of the `print()`
109function. To do so, we have to re-declare the template parameters:
110
111```
112template<typename S, typename T>
113class Pair {
114public:
115 S first;
116 T second;
117
118 Pair(S s, T t) : first{s}, second{t} {}
119
120 void print() const;
121};
122
123template<typename S, typename T>
124void Pair<S, T>::print() const {
125 std::cout << "(" << first << ", " << second << ")";
126}
127```
128
129The syntax is not amazing, but it can be worth it for longer functions.
130
131### Function templates
132
133Templates are not limited to classes, we can also have function templates.
134A classic example is a
135[swap](https://en.cppreference.com/w/cpp/algorithm/swap) function, which
136can be implemented like this:
137
138```
139template<typename T>
140void swap(T& a, T& b) {
141 T tmp = a;
142 a = b;
143 b = tmp;
144}
145```
146
147(Here I am using
148[references](https://en.cppreference.com/w/cpp/language/reference),
149which in case you don't know are just a simplified syntax for pointers.)
150
151Let's take this simple example to show an important property of templates.
152Let's say that, perhaps by mistake, we implemented the swap function
153using different template types for `a` and `b` (see
154[swap.cpp](https://git.tronto.net/taming-cpp/file/templates/swap.cpp.html)):
155
156```
157template<typename S, typename T>
158void swap(S& a, T& b) {
159 S tmp = a;
160 a = b;
161 b = tmp;
162}
163```
164
165This is actually ok, because **templates are not type-checked until
166they are specialized**. And in fact we may legitimately want to
167swap variables of different types, for example:
168
169```
170int x = 3;
171double y = 1.0;
172swap(x, y);
173```
174
175The code above will compile just fine, because C++ accepts implicit type
176conversions between `int` and `double`. However, this:
177
178```
179int x = 3;
180std::string y = "1.0";
181swap(x, y);
182```
183
184Will not compile:
185
186```
187swap.cpp:6:6: error: assigning to 'int' from incompatible type 'std::basic_string<char>'
188 6 | a = b;
189 | ^
190swap.cpp:13:2: note: in instantiation of function template specialization 'swap<int, std::basic_string<char>>' requested here
191 13 | swap(x, y);
192 | ^
1931 error generated.
194```
195
196The error messages we get when misusing templates are not always nice
197and readable as this one, they can literaly be hundreds of lines long.
198Luckily, C++20 introduced
199[constraints and concepts](https://en.cppreference.com/w/cpp/language/constraints)
200to make these error messages more meaningful - we'll see some examples below.
201
202### Non-type parameters
203
204Objects, not just types, can be template parameters. A classic example
205is [`std::array`](https://en.cppreference.com/w/cpp/container/array),
206a fixed-size container where the capacity is fixed at compile time (see
207[std_array.cpp](https://git.tronto.net/taming-cpp/file/templates/std_array.cpp.html)
208for an example).
209
210Non-type parameters can be constants of any *structural type* - see
211[this page](https://en.cppreference.com/w/cpp/language/template_parameters)
212for a precise definition. Remember that you can only specialize them
213with compile-time (i.e. `constexpr`) constants!
214
215With non-type parameter you can do pretty wild stuff, see for example
216[factorial.cpp](https://git.tronto.net/taming-cpp/file/templates/factorial.cpp.html)
217- although this specific example is not very useful, since it can easily
218be replaced by a constexpr function.
219
220Fun fact: if you use `auto`, you don't even have to specify a type for
221a non-type parameter. For example, the following code works just fine (see
222[println.cpp](https://git.tronto.net/taming-cpp/file/templates/println.cpp.html)):
223
224```
225#include <iostream>
226
227template<auto X> void println() { std::cout << X << std::endl; }
228
229int main() {
230 println<1.23>();
231 println<42>();
232
233 return 0;
234}
235```
236
237Later we'll see a more useful application of this.
238
239### Default values
240
241Template parameters can have default value, for example:
242
243```
244template<typename S = int, typename T = S>
245class Pair {
246public:
247 S first;
248 T second;
249
250 // And so on...
251```
252
253With the code above, `Pair` is going to denote a pair of two integers,
254and `Pair<double>` is going to denote a pair of two doubles.
255
256Non-type parameters can have default values too:
257
258```
259template<typename T, int N = 10>
260class MyArray {
261 // A container with 10 elements by default
262};
263```
264
265### Variadic templates
266
267Like with functions, templates can have a variable number of parameters.
268A classic example is
269[`std::tuple`](https://en.cppreference.com/w/cpp/utility/tuple), which
270works similarly to `std::pair`, but accepts any number of items.
271
272## Contraints and concepts
273
274To explain the last features I want to talk about, I am going to use a
275simlpe, albeit slightly unusual, example: let's implement a class
276template for
277[the integers modulo `N`](https://en.wikipedia.org/wiki/Modular_arithmetic),
278where `N` is a fixed at compile-time.
279
280We may start with something like this (see
281[zmodn-1.cpp](https://git.tronto.net/taming-cpp/file/templates/zmodn-1.cpp.html)):
282
283```
284#include <iostream>
285#include <optional>
286#include <tuple>
287
288std::tuple<int, int, int> extended_gcd(int a, int b) {
289 if (b == 0) return {a, 1, 0};
290 auto [g, x, y] = extended_gcd(b, a%b);
291 return {g, y, x - y*(a/b)};
292}
293
294template<int N>
295class Zmod {
296public:
297 int value;
298
299 Zmod(int z) : value{(z%N + N) % N} {}
300
301 Zmod operator+(const Zmod& z) const { return value + z.value; }
302 Zmod operator-(const Zmod& z) const { return value - z.value; }
303 Zmod operator*(const Zmod& z) const { return value * z.value; }
304
305 std::optional<Zmod> inverse() const {
306 auto [g, a, _] = extended_gcd(value, N);
307 return g == 1 ? Zmod(a) : std::optional<Zmod>{};
308 }
309
310 std::optional<Zmod> operator/(const Zmod& d) const {
311 auto i = d.inverse();
312 return i ? (*this) * i.value() : i;
313 }
314
315 std::optional<Zmod> operator/=(const Zmod& d) {
316 auto q = *this / d;
317 return q ? (*this = q.value()) : q;
318 }
319};
320
321int main() {
322 Zmod<57> x(34);
323 Zmod<57> y(11);
324
325 std::cout << "34 * 11 = " << (x * y).value << " (mod 57)" << std::endl;
326
327 if (auto inv = y.inverse(); inv)
328 std::cout << "11 * " << inv.value().value << " = 1 (mod 57)" << std::endl;
329 else
330 std::cout << "11 is not invertible in Z/57Z" << std::endl;
331
332 return 0;
333}
334```
335
336So we are just using a single non-type template parameter, no big deal.
337
338But now let's say that by accident I type something like:
339
340```
341int main() {
342 Zmod<0> z(13); // Oops, I meant Zmod<10>
343}
344```
345
346Unfortunately, this code is going to compile just fine, and I'll get
347a horrible run-time error. It would be cool if there was some way
348to *constrain* the template argument to only allow positive integers.
349Which brings us to...
350
351### Constraints
352
353[Constraints](https://en.cppreference.com/w/cpp/language/constraints)
354are a way to prevent nasty run-time errors and / or make compiler errors
355more meaningful when using templates; they were added in C++20.
356
357In our case, introducing our constraint is quite simple (see
358[zmodn-2.cpp](https://git.tronto.net/taming-cpp/file/templates/zmodn-2.cpp.html)):
359
360```
361template<int N>
362requires (N > 1)
363class Zmod {
364 // Same as before...
365};
366```
367
368And now if we try to compile the `Zmod<0>` declaration we get:
369
370```
371zmodn-2.cpp:51:2: error: constraints not satisfied for class template 'Zmod' [with N = 0]
372 51 | Zmod<0> z(157);
373 | ^~~~~~~
374zmodn-2.cpp:12:11: note: because '0 > 1' (0 > 1) evaluated to false
375 12 | requires (N > 1)
376 | ^
3771 error generated.
378```
379
380Nice!
381
382### Making it more generic
383
384For one specific application of this class, which I may write about in
385a future post, I need `N` to be very larger, larger than a 32-bit
386integer. So I should probably change `int` to `long long` or `int64_t`.
387Except I would like to work with numbers that are even larger than 64
388bits! This means I should find (or write) a library for large integers,
389but at the same time I want to keep the `Zmod` class independent of a
390specific library... I should definitely make `Zmod` parametric in the
391type of N.
392
393In order to do so, I can use a non-type parameter declared `auto` and
394[`decltype()`](https://en.cppreference.com/w/cpp/language/decltype) (see
395[zmodn-3.cpp](https://git.tronto.net/taming-cpp/file/templates/zmodn-3.cpp.html)):
396
397```
398template<auto N>
399requires (N > 1)
400class Zmod {
401public:
402 decltype(N) value;
403
404 Zmod(decltype(N) z) : value{(z%N + N) % N} {}
405
406 // The rest is unchanged
407};
408```
409
410And of course I should also templatize the `extended_gcd()` function -
411you can see the full code in
412[zmodn-3.cpp](https://git.tronto.net/taming-cpp/file/templates/zmodn-3.cpp.html).
413
414Now we can use any type as a "base" for our modular integer! Well, almost.
415I mentioned above that the type we use must be *structural*, but that is
416relatively easy to satisfy. A bigger problem is that our type must
417allow for compile-time constants - so we need, at least, a `constexpr`
418constructor. I could not find a suitable library online, so I ended
419up writing my own - see
420[bigint.h](https://git.tronto.net/taming-cpp/file/templates/bigint.h.html).
421
422The code is simple and not very efficient, but this library is not meant
423to be efficient. I am just using it for educational purposes.
424
425To show it off, we can do stuff like this:
426
427```
428int main() {
429 constexpr BigInt N("1000000000000000000000000000000");
430 Zmod<N> x(BigInt("123456781234567812345678"));
431 Zmod<N> y(BigInt("987654321987654321"));
432
433 std::cout << x.value << " * "
434 << y.value << " (mod " << N << ") = "
435 << (x * y).value << std::endl;
436
437 // Prints:
438 // 123456781234567812345678 * 987654321987654321 (mod 1000000000000000000000000000000) = 5237873798636805364022374638
439
440 return 0;
441}
442```
443
444But now we have once again a constraint problem. We could for example write
445
446```
447 constexpr double M = 3.14;
448 Zmod<M> z(M);
449```
450
451And sure, this will fail with an understandable error message related to
452the modulo operation `%`, but it is not hard to imagine that in other
453situations this could be a problem. So we should put a constraint on
454our type, in this case `decltype(N)`.
455
456At first I achieved this using the *type trait*
457[`std::is_integral`](https://en.cppreference.com/w/cpp/types/is_integral):
458
459```
460template<auto N>
461requires (N > 1) && std::is_integral<decltype(N)>::value
462class Zmod {
463 // Etc...
464};
465```
466
467Type traits are defined in the `<type_traits>` header. For an overview
468check out this nice
469[blog post](https://www.internalpointers.com/post/quick-primer-type-traits-modern-cpp).
470
471Unfortunately, my custom big integer class does not satisfy
472`std::is_integral`. So I have to define my own set of constraints.
473
474### Concepts
475
476Along with constraints, C++20 also introduced the possibility to define
477and name a custom set of requirements. This can be done with *concepts*.
478In our example, we can require that our type supports all the operations
479we need (see
480[zmodn-4.cpp](https://git.tronto.net/taming-cpp/file/templates/zmodn-4.cpp.html)):
481
482```
483template<typename T>
484concept Integer = requires(T a, T b, int i) {
485 {T(i)};
486
487 {a + b} -> std::same_as<T>;
488 {a - b} -> std::same_as<T>;
489 {a * b} -> std::same_as<T>;
490 {a / b} -> std::same_as<T>;
491 {a % b} -> std::same_as<T>;
492
493 {a == b} -> std::same_as<bool>;
494 {a != b} -> std::same_as<bool>;
495};
496```
497
498Let's break this down. The first line introduces a template, because our
499concept depends on a type parameter `T`. The second line introduces the
500definition of the concept: the arguments to the `requires` keyword are
501variables that we are going to use in our concept definition.
502
503The third line is where things get interesting. The notation `{T(i)}`
504means "`T(i)` must be a valid expression", where `i` is any variable of
505type `int`, as defined in the `requires` arguments. In other words, we
506are asking that type `T` has a constructor that takes a single integer
507parameter.
508
509The other lines are all similar, and they expand on this concept.
510For example `{a % b} -> std::same_as<T>` requires that the operator `%` is
511defined between variables of type T; moreover, the `-> std::same_as<T>`
512notation declares that we want the resulting type to satisfy the
513type trait `std::same_as<T>` - in other words, we are asking for
514`T operator%(T)` to be defined as a member function of `T`.
515
516Note that we are not requiring anything about what these operations
517actually do: we are only requiring that they are defined. If for some
518reason a custom floating point type defines an operator `%` and all other
519arithmetic operations that we require, it could be used for our `zmod<N>`
520class without any complaints from the compiler, but the results may not
521be what we expect.
522
523We can use our newly-defined concept in two ways. As part of a requires
524clause:
525
526```
527template<typename T>
528requires Integer<T>
529std::tuple<T, T, T> extended_gcd(T a, T b) { /* Same as before */ }
530
531template<auto N>
532requires (N > 1) && Integer<decltype(N)>
533class Zmod { /* Same as before */ }
534```
535
536Or with the following syntax sugar:
537
538```
539template<Integer T>
540std::tuple<T, T, T> extended_gcd(T a, T b) { /* Same as before */ }
541
542template<Integer auto N>
543requires(N > 1)
544class Zmod { /* Same as before */ }
545```
546
547I tend to prefer the second way because it is more compact and it reads
548nicely: in the first of the two templates, we declare `T` as if it were
549a variable of type `Integer`.
550
551And now if we try to compile `Zmod<3.14>` the compiler gives us a clear
552error message:
553
554```
555zmodn-4.cpp:68:3: error: constraints not satisfied for class template 'Zmod' [with N = 3.140000e+00]
556 68 | Zmod<M> z(4);
557 | ^~~~~~~
558zmodn-4.cpp:29:10: note: because 'decltype(3.1400000000000001)' (aka 'double') does not satisfy 'Integer'
559 29 | template<Integer auto N>
560 | ^
561zmodn-4.cpp:16:5: note: because 'a % b' would be invalid: invalid operands to binary expression ('double' and 'double')
562 16 | {a % b} -> std::same_as<T>;
563 | ^
5641 error generated.
565```
566
567## Conclusion
568
569Templates are a very powerful tool that allow creating zero-cost
570abstractions (if you don't count the longer compile time as a cost).
571With the addition of constraints and concepts in C++20 it became easier to
572define requirements on the template parameters and catch template misuse
573at compile time.
574
575In some way concepts offer a new style of abstraction, similar in scope
576to object-oriented programming. Since I am not a fan of OOP, I like that
577we have this new option, and I am going to play around with whenever I
578get the chance.
diff --git a/src/series/series.md b/src/series/series.md
index 84bdad0..afbf3dd 100644
--- a/src/series/series.md
+++ b/src/series/series.md
@@ -49,3 +49,4 @@ My adventures in learning C++ as a C programmer.
49 49
50* Episode 1: [motivation](../blog/2024-04-30-taming-cpp-motivation) 50* Episode 1: [motivation](../blog/2024-04-30-taming-cpp-motivation)
51* Episode 2: [RAII](../blog/2024-12-26-taming-cpp-raii) 51* Episode 2: [RAII](../blog/2024-12-26-taming-cpp-raii)
52* Episode 3: [templates, constraints and concepts](../blog/2025-01-21-taming-cpp-templates)

Generated with cgit - Back to sebastiano.tronto.net