Skip to content

Commit 57f34bf

Browse files
authored
Lecture on using templates in compiled libraries (#97)
1 parent 9b0b84d commit 57f34bf

7 files changed

+360
-0
lines changed
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

lectures/images/CompilerIndepth.png

Lines changed: 3 additions & 0 deletions
Loading

lectures/templates_and_headers.md

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
**Templates in header and source files**
2+
3+
----
4+
5+
<p align="center">
6+
<a href="https://youtu.be/blah"><img src="https://img.youtube.com/vi/blah/maxresdefault.jpg" alt="Video" align="right" width=50% style="margin: 0.5rem"></a>
7+
</p>
8+
9+
- [Why linker fails](#why-linker-fails)
10+
- [Compilation process for single `main.cpp` file](#compilation-process-for-single-maincpp-file)
11+
- [Compilation process for multiple files](#compilation-process-for-multiple-files)
12+
- [How to fix the linker error](#how-to-fix-the-linker-error)
13+
- [More complex explicit instantiations](#more-complex-explicit-instantiations)
14+
- [Summary](#summary)
15+
16+
17+
In the previous videos, we talked quite extensively about templates and we should already know what they are, what they do under the hood and how to use templates in our code.
18+
19+
So, writing some simple templated code like this should come natural to us:
20+
```cpp
21+
// Function template.
22+
template <typename T>
23+
void Foo() {
24+
// Do something.
25+
}
26+
27+
int main() {
28+
Foo<int>();
29+
Foo<float>();
30+
return 0;
31+
}
32+
```
33+
34+
With all of this knowledge, we might want to write some slightly more complicated software. Which means that we will eventually want to compile our code into *libraries* and *link* these libraries to executable files.
35+
36+
So we might try to split our files into header and source files just like we used to do with non-template code.
37+
38+
<table>
39+
<tr>
40+
<td>
41+
42+
`foo.hpp`
43+
<!--
44+
`CPP_SETUP_START`
45+
$PLACEHOLDER
46+
`CPP_SETUP_END`
47+
`CPP_COPY_SNIPPET` template_library_simple/foo.hpp
48+
-->
49+
```cpp
50+
#pragma once
51+
52+
// Function template declaration.
53+
template <typename T>
54+
void Foo();
55+
```
56+
57+
`foo.cpp`
58+
<!--
59+
`CPP_SETUP_START`
60+
$PLACEHOLDER
61+
template void Foo<int>();
62+
template void Foo<float>();
63+
`CPP_SETUP_END`
64+
`CPP_COPY_SNIPPET` template_library_simple/foo.cpp
65+
`CPP_RUN_CMD` CWD:template_library_simple c++ -std=c++17 -c foo.cpp -o foo.o
66+
-->
67+
```cpp
68+
#include "foo.hpp"
69+
70+
// Function template definition.
71+
template <typename T>
72+
void Foo() {
73+
// Do something.
74+
}
75+
```
76+
</td>
77+
78+
<td>
79+
80+
`main.cpp`
81+
<!--
82+
`CPP_SETUP_START`
83+
$PLACEHOLDER
84+
`CPP_SETUP_END`
85+
`CPP_COPY_SNIPPET` template_library_simple/main.cpp
86+
`CPP_RUN_CMD` CWD:template_library_simple c++ -std=c++17 main.cpp foo.o -o main
87+
-->
88+
```cpp
89+
#include "foo.hpp"
90+
91+
int main() {
92+
Foo<int>();
93+
Foo<float>();
94+
return 0;
95+
}
96+
```
97+
For simplicity of this example, we compile both our `foo.cpp` and `main.cpp` into binary object files `foo.o` and `main.o` and provide these to our compiler (which passes it internally to the linker) when building our executable:
98+
<!--
99+
`CPP_SKIP_SNIPPET`
100+
-->
101+
```bash
102+
# Create object file foo.o.
103+
c++ -std=c++17 -c foo.cpp -o foo.o
104+
# Create object file main.o.
105+
c++ -std=c++17 -c main.cpp -o main.o
106+
# Link object files to produce an executable
107+
c++ -std=c++17 main.o foo.o -o main
108+
```
109+
110+
</td>
111+
</tr>
112+
</table>
113+
114+
115+
It all looks quite logical, but there is a problem: when we run it we get our linker complaining about undefined symbols that we reference in our `main` function:
116+
```css
117+
Undefined symbols for architecture arm64:
118+
"void Foo<float>()", referenced from:
119+
_main in main-cebf24.o
120+
"void Foo<int>()", referenced from:
121+
_main in main-cebf24.o
122+
ld: symbol(s) not found for architecture arm64
123+
clang: error: linker command failed with exit code 1 (use -v to see invocation)
124+
```
125+
126+
The same would have happened if we would have packed our object file into a library and linked this library explicitly to our executable. See the lecture about [libraries](headers_and_libraries.md) if you find this confusing.
127+
128+
Anyway, today, we dive into why this linker error happens and what we can do about it. Because there **is** a way to compile templated code into libraries after all :wink:
129+
130+
<!-- Intro -->
131+
132+
## Why linker fails
133+
Before we start, we should make sure we understand the compilation process, especially when templates are involved, well enough as this is crucial to understand why the linker fails. We talk about this extensively in the lecture about [what templates do under the hood](templates_what.md) and I urge you to look at that lecture before we continue here. It will make this lecture much easier to digest.
134+
135+
### Compilation process for single `main.cpp` file
136+
That being said, we will now go through the whole compilation process, starting with the simple case of compiling a single `main.cpp` file that contains all of our code and progressively making our example more complicated.
137+
138+
Any compilation process consists really of 3 stages:
139+
1. Preprocessor creates translation units, `main.s` in our case
140+
2. Compiler compiles the code into binary object files, `main.o` in our case
141+
3. Linker links various symbols, potentially across multiple binaries to produce the `main` executable file
142+
143+
<p align="center">
144+
<img src="images/CompilationProcessEverything.png" alt="Video" style="width:100%;max-width:800;">
145+
</p>
146+
147+
At this point, it is important to look a bit closer at what the **compiler** does here. When it sees the templates, it looks at which type we instantiate these templates with in our code and, well, creates implicit instantiations of these templates with the needed types. In our case, we use the function template `Foo` with the types `int` and `float`. These are then compiled directly into the object file alongside all the rest of the code as can be seen if we look at the **Symbol Table** of the resulting object file, which can be done by using either the `objdump` command.
148+
149+
These are the symbols we need in order to correctly compile our executable!
150+
151+
<p align="center">
152+
<img src="images/CompilerIndepth.png" alt="Video" style="width:100%;max-width:800;">
153+
</p>
154+
155+
156+
### Compilation process for multiple files
157+
Armed with this knowledge, we can look at what happens if we naively try to separate our template implementation into `foo.hpp` and `foo.cpp` files, away from the `main.cpp` file that now holds only the `main` function.
158+
159+
We start by compiling the `foo.cpp` file, which contains the definitions of our `Foo` function and includes the file `foo.hpp`, which contains the declaration of the function template `Foo`.
160+
161+
<p align="center">
162+
<img src="images/CompilerFooNotKnowing.png" alt="Video" style="width:100%;max-width:800;">
163+
</p>
164+
165+
As we already know, the preprocessor creates a **translation unit**, which essentially means that it just copies the contents of the `foo.hpp` into the `foo.cpp`, removes comments etc. As before, we will show this file as `foo.s` here. By default this file is not saved to disk but we _can_ save it using `--save-temps` flag that we can pass to the compiler.
166+
167+
Anyway, this translation unit is then passed into the compiler, which generates the `foo.o` object file.
168+
169+
The compiler sees our function template declaration and definition, however, this time around the compiler **does not see any code that asks it to instantiate our templates! And so it doesn't!** If we inspect the symbol table of the generated `foo.o` object file, we will see no `Foo` related symbols in it!
170+
171+
Now, if we do the same compilation process for our new `main.cpp` that also includes `foo.hpp` and try to link the resulting `main.o` object file with the `foo.o` object file from the previous step, **we get the linker error!**
172+
173+
<p align="center">
174+
<img src="images/CompilationMainWithFooFail.png" alt="Video" style="width:100%;max-width:800;">
175+
</p>
176+
177+
And by now we should see that the reason is that the `foo.o` does not have the symbols that we need in our `main` function, namely the specializations of our `Foo` template for `int` and for `float`.
178+
179+
## How to fix the linker error
180+
Linker errors like these prompt many people to just use header-only libraries when using templates. And this _might_ be the only available solution if the library we are writing *really has to be extremely generic* and we have no idea which types our templates might be used with.
181+
182+
But this is not always the case. Actually, this is most often **not** the case! Most of us mortals don't write extremely generic libraries. We use templates to avoid code duplication and to introduce some nice-to-read abstractions into our system. In such situations, we usually know exactly what types we might be using with our templates!
183+
184+
In this situations we have another tool in our toolbox, called the **explicit template instantiation**, which can be used to, well, explicitly instantiate templates.
185+
186+
In our case, the explicit instantiation for our `Foo` function template looks like this:
187+
<!--
188+
`CPP_SKIP_SNIPPET`
189+
-->
190+
```cpp
191+
template void Foo<int>();
192+
template void Foo<float>();
193+
```
194+
Essentially, syntax-wise it looks like something stuck between a function declaration (just with a `template` keyword before it) and a template specialization (just without the angular brackets after the word `template`). To avoid any confusion, I would suggest to refresh how [function template specialization](templates_how_functions.md#full-function-template-specialization-and-why-function-overloading-is-better) looks like. What it essentially does is it forces the compiler to generate the code for our template with the provided type without waiting for seeing the code that uses it within some function.
195+
196+
And hopefully by now you can guess what we need to do to fix our linker error - we need to add **explicit template instantiations** to the end of our `foo.cpp` file:
197+
<!--
198+
`CPP_SETUP_START`
199+
$PLACEHOLDER
200+
`CPP_SETUP_END`
201+
`CPP_COPY_SNIPPET` template_library_simple/foo_new.cpp
202+
`CPP_RUN_CMD` CWD:template_library_simple c++ -std=c++17 -c foo_new.cpp -o foo_new.o && c++ -std=c++17 foo_new.o main.cpp
203+
-->
204+
```cpp
205+
#include "foo.hpp"
206+
207+
// Function template definition.
208+
template <typename T>
209+
void Foo() {
210+
// Do something.
211+
}
212+
213+
template void Foo<int>();
214+
template void Foo<float>();
215+
```
216+
It is common to add these instantiations to the end of the file as by that time the compiler has seen all of the definitions so it knows how to generate the required template instantiations, which are then available in the symbol table of the `foo.o` object file. These symbols are then available for linkage against those required from the `main.o` object file to produce the final executable without error! :tada:
217+
218+
219+
<p align="center">
220+
<img src="images/CompilationSuccessWithHeaders.png" alt="Video" style="width:100%;max-width:800;">
221+
</p>
222+
223+
## More complex explicit instantiations
224+
This covers the basics of why we might want to use explicit template instantiation as well as hints on how this can be achieved.
225+
226+
But I guess you might be wondering if this only works with functions and the answer is: of course not! We can explicitly instantiate function, classes and even class methods.
227+
228+
Instead of talking about it too abstractly, let us look at a slightly more complex example which covers most of the interesting use-cases. The rest, I'm sure, you'll be able to figure out on your own.
229+
230+
Here, in the new `foo.hpp` header, we have a struct template that has a method template. In addition to the struct we also have a function that returns an object of our struct template and accepts a parameter of some type.
231+
232+
`foo.hpp`
233+
<!--
234+
`CPP_SETUP_START`
235+
$PLACEHOLDER
236+
`CPP_SETUP_END`
237+
`CPP_COPY_SNIPPET` complex_template_library/foo.hpp
238+
-->
239+
```cpp
240+
#pragma once
241+
// Class template declaration.
242+
template <typename T>
243+
struct Foo {
244+
template <typename S>
245+
void Bar(const S& smth) const;
246+
247+
T data{};
248+
};
249+
250+
// Function template declaration.
251+
template <typename T>
252+
Foo<T> MakeFoo(const T& smth);
253+
```
254+
Just as before, the definitions for all of these declarations live in the `foo.cpp` file. And, as we know by now, we have to also explicitly instantiate all of these. Here, we make sure that we instantiate the struct template `Foo` for the type `int`, its `Bar` method for the types `int` and `float` (note that for functions we can either provide the type explicitly or let it be figured out from the function parameters) and, finally, the `MakeFoo` function for the type `int`.
255+
256+
`foo.cpp`
257+
<!--
258+
`CPP_SETUP_START`
259+
$PLACEHOLDER
260+
`CPP_SETUP_END`
261+
`CPP_COPY_SNIPPET` complex_template_library/foo.cpp
262+
`CPP_RUN_CMD` CWD:complex_template_library c++ -std=c++17 -c foo.cpp -o foo.o
263+
-->
264+
```cpp
265+
#include "foo.hpp"
266+
267+
// Class method template definition.
268+
template <typename T>
269+
template <typename S>
270+
void Foo<T>::Bar(const S& smth) const {}
271+
272+
// Function template definition.
273+
template <typename T>
274+
Foo<T> MakeFoo(const T& smth) {
275+
return Foo<T>{smth};
276+
}
277+
278+
template class Foo<int>;
279+
template void Foo<int>::Bar<int>(const int&) const;
280+
template void Foo<int>::Bar(const float&) const;
281+
template Foo<int> MakeFoo<int>(const int&);
282+
```
283+
And of course we also have a `main` function that makes use of all of the instantiations from before.
284+
285+
`main.cpp`
286+
<!--
287+
`CPP_SETUP_START`
288+
$PLACEHOLDER
289+
`CPP_SETUP_END`
290+
`CPP_COPY_SNIPPET` complex_template_library/main.cpp
291+
`CPP_RUN_CMD` CWD:complex_template_library c++ -std=c++17 main.cpp foo.o -o main
292+
-->
293+
```cpp
294+
#include "foo.hpp"
295+
296+
int main() {
297+
const int data = 42;
298+
const auto foo = MakeFoo(data);
299+
foo.Bar(data);
300+
foo.Bar(42.42F);
301+
return 0;
302+
}
303+
```
304+
305+
Now if we build the object files for the `foo.cpp` and `main.cpp` we can link them together to produce the resulting binary without issues as all of our symbols have been generated after our templates have been explicitly instantiated by us in the `foo.cpp` file.
306+
<!--
307+
`CPP_SKIP_SNIPPET`
308+
-->
309+
```bash
310+
# Create object file foo.o.
311+
c++ -std=c++17 -c foo.cpp -o foo.o
312+
# Create object file main.o.
313+
c++ -std=c++17 -c main.cpp -o main.o
314+
# Link object files.
315+
c++ -std=c++17 main.o foo.o -o main
316+
```
317+
318+
I'll leave it up to you to inspect the symbol tables of the generated binary files as well as to play around with this example by changing the template declarations and definitions as well as any arguments that we pass around. As always, it's all about getting a feeling for what works as well as how and why things break.
319+
320+
## Summary
321+
This lecture should hopefully be enough to give us some intuition about explicit template instantiation and how it allows to split our template code declarations and definitions to header and source files.
322+
323+
For more details, I'll refer you to cppreference.com, as always. There are two distinct pages there with all the information we might be interested in, one for [explicit instantiations of function templates](https://en.cppreference.com/w/cpp/language/function_template#Explicit_instantiation) and one for [explicit instantiations of class templates](https://en.cppreference.com/w/cpp/language/class_template#Explicit_instantiation).
324+
325+
<!-- You'll also find the links to these below the video of course. And, as always, if you do run into any issues or come up with any questions, please write them in the comments, I value any feedback and will try my best to explain anything that was not clear enough.
326+
327+
328+
And with this, I'd like to thank you for your attention, hope you liked what you saw today and if you feel that some details still didn't fully clock, then why not refresh your understanding of how to use templates by re-watching one of these videos?
329+
-->

readme.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,22 @@ Headers with classes
569569
----------------------------------------------------------
570570
</details>
571571

572+
<details>
573+
<summary>Header and source files for templated code</summary>
574+
575+
----------------------------------------------------------
576+
[![Video thumbnail](https://img.youtube.com/vi/blah/maxresdefault.jpg)](https://youtu.be/blah)
577+
578+
- [Why linker fails](lectures/templates_and_headers.md#why-linker-fails)
579+
- [Compilation process for single `main.cpp` file](lectures/templates_and_headers.md#compilation-process-for-single-maincpp-file)
580+
- [Compilation process for multiple files](lectures/templates_and_headers.md#compilation-process-for-multiple-files)
581+
- [How to fix the linker error](lectures/templates_and_headers.md#how-to-fix-the-linker-error)
582+
- [More complex explicit instantiations](lectures/templates_and_headers.md#more-complex-explicit-instantiations)
583+
- [Summary](lectures/templates_and_headers.md#summary)
584+
585+
----------------------------------------------------------
586+
</details>
587+
572588
## PS
573589

574590
### Most of the code snippets are validated automatically

0 commit comments

Comments
 (0)