Skip to content

Commit

Permalink
Add images and finalize text
Browse files Browse the repository at this point in the history
  • Loading branch information
niosus committed Feb 3, 2024
1 parent 5f6028c commit 69a961b
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 31 deletions.
3 changes: 3 additions & 0 deletions lectures/images/Compilation_process.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions lectures/images/Compiler.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 49 additions & 31 deletions lectures/templates_what.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ What templates do under the hood
- [What templates do under the hood](#what-templates-do-under-the-hood)
- [Compilation process recap](#compilation-process-recap)
- [Compiler uses templates to generate code](#compiler-uses-templates-to-generate-code)
- [Hands-on example of what gets compiled](#hands-on-example-of-what-gets-compiled)
- [Compiler generates code from templates in a lazy way](#compiler-generates-code-from-templates-in-a-lazy-way)
- [Hands-on example](#hands-on-example)
- [Try it out!](#try-it-out)
- [Compiler is lazy](#compiler-is-lazy)
- [Summary](#summary)


Expand All @@ -21,30 +22,39 @@ Today, we focus on the **what**. We will try to build an intuition about what th

## Compilation process recap
First, let's do a small recap of how our executables are generated. We already talked about the full compilation process [before](headers_and_libraries.md) but the gist is that all of our input source files pass through three stages to become executable files:
```mermaid
graph LR
Start:::hidden -- source files --> Preprocessor
Preprocessor -- translation units --> Compiler
Compiler -- object files --> Linker
Linker -- executable files --> END:::hidden

classDef hidden display: none;
style Compiler fill:#0066aa,stroke-width:0px
```
<img src="images/Compilation_process.png" alt="Video" align="center" width=500>

They start by going through the **preprocessor** that unwraps any macros and includes and creates translation units from our source files. These files are then transformed into object files by the **compiler**. Finally, the object files get linked together by the **linker** to form the actual executables.

## Compiler uses templates to generate code
Everything that we will discuss today happens in the **compiler**. So, let's discuss **what** it does when we use a function or class template.

And the simplified answer is surprisingly easy:
> 🚨 The compiler uses templates to generate and compile code. Which also explains their name. A function, struct or a class template is just that - a template for a normal function, struct or a class.
<!-- The intuition is that -->
> 🚨 **Intuition:** compiler uses templates to generate code. Which also explains their name. A function, struct or a class template is just that - a template for a normal function, struct or a class.
While this **is** a simplified rule of thumb, it serves a good intuition for what happens to our templates during compilation.
```mermaid
graph LR
F[Function template] ---> F1[Function 1]
F ---> F2[Function 2]
F ---> F3[Function 3]
F ---> FF[...]
```

To be slightly more precise, but still staying in the intuitive explanations realm, any time the compiler encounters a call that it associates to a template, it **instantiates** a concrete **specialization** of that function, struct or class substituting all of its template arguments for the actually used types. Such specializations are then compiled into their binary form and behave in a very similar way to normal functions, structs or classes. This, however, means that if we have, say, a template function and use it with many types we will have many copies of that function compiled into our binary - one for each combination of types it is used with.
While this **is** a simplified rule of thumb, it serves as a good intuition for what happens to our templates during compilation.

## Hands-on example of what gets compiled
For completeness, let's also illustrate it one more time by inspecting a binary that we get from actual code that we compile by hand. Well, ok, not by hand but by using a compiler directly :smile:. For that we will go back to our `maximum.cpp` file from the previous lecture:
<!-- Use animation -->
To be slightly more precise, let us illustrate what happens to a simple function template `Foo` during the compilation.

<img src="images/Compiler.png" alt="Video" align="center" width=500>

<!-- Add a picture here -->
Any time the compiler encounters a call that it associates to a template, it **instantiates** a concrete **specialization** of that function, substituting all of its template arguments for the actually used types. Such specializations are then compiled into their binary form and for all means and purposes behave just like normal functions. If we inspect the resulting object file, we will see all of our concrete functions in it!

This, however, means that if we have, say, a function template and use it with many types we will have many specializations of that function compiled into our binary - one for each combination of types it is used with.

## Hands-on example
For completeness, let's now see how we can check these things by compiling some real code by hand. Well, ok, not by hand but by using a compiler directly from the command line :smile:. For that we will go back to our `maximum.cpp` file from the previous lecture:
```cpp
template <typename NumberType>
NumberType Maximum(NumberType first, NumberType second) {
Expand All @@ -58,11 +68,13 @@ int main() {
Maximum(3.14, 42.42);
}
```
Which we can compile from the terminal in the same way we compiled examples before (note the `-c` flag, we are only interested in compilation into an object file):
```cmd
c++ -std=c++17 -c -o maximum maximum.cpp
It defines the `Maximum` function template and uses it with 3 different types. Not the most useful code but a good illustration.
We can compile it from the terminal in the same way we compiled examples before (note the `-c` flag, we are only interested in compilation into an object file):
```bash
# generates maximum.o object file
c++ -std=c++17 -c maximum.cpp
```
This produces an object file `maximum.o`. This object file is just a binary file in `ELF` format on Linux, or `Mach-O` format on MacOS, at least by default. But for our purposes, both will equally do. We can inspect these files with `objdump` (or alternatively `nm` ) command. At this point, we are interested in looking at the part of this file that lists all the available symbols - **a symbols table**. We can read it by providing the appropriate flags to the `objdump` executable (`-t` to get a symbols table and `-C` to get better looking symbol names):
This produces an object file `maximum.o`. This object file is just a binary file in [`ELF`](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format) format on Linux, or [`Mach-O`](https://en.wikipedia.org/wiki/Mach-O) format on MacOS, at least by default. But for our purposes, both will equally do. We can inspect these files with the `objdump` command. At this point, we are interested in looking at the part of this file that lists all the available symbols - **a symbols table**. We can read it by providing the appropriate flags to the `objdump` executable (`-t` to get a symbols table and `-C` to get better looking symbol names) and we expect to see all the compiled functions briefly mentioned there:
<!--
`CPP_SKIP_SNIPPET`
-->
Expand All @@ -78,30 +90,36 @@ SYMBOL TABLE:
0000000000000050 w F __TEXT,__text int Maximum<int>(int, int)
0000000000000000 g F __TEXT,__text _main
```
Now if we look carefully at this output (I trimmed it a little), we see that there is the `_main` symbol for our `main` function as well as three different `Maximum` functions with the types that match those that we used in the `main` function: `int`, `double`, and `float`.
Now if we look carefully at this output (which I trimmed a little), we see that there is the `_main` symbol for our `main` function as well as three different `Maximum` functions with the types that match those that we used in the `main` function: `int`, `double`, and `float`.

Furthermore, if we run this code in the excellent [compiler explorer](https://godbolt.org/z/9zjoTh7Kq) (link brings you to the above example) we can also see the actual calls to the compiler-generated concrete function specializations (look for `call` statements). The other details in the output are not too important for us for now.

Please try it out yourself, so that you're sure that I'm not lying to you :wink: Really, you see that these examples take minutes to write and you can learn so much from them! Don't be afraid to fail, try other functions, try more types, try class and struct templates and call their methods that might have templates of their own! You don't even need to read the documentation at this point, just try what feels logical!
## Try it out!
Please do try it all out yourself, so that you're sure that I'm not lying to you :wink: Really, you see that these examples take minutes to write and you can learn so much from them! Don't be afraid to fail, try other functions, try more types, try class and struct templates and call their methods that might have templates of their own! You don't even need to read the documentation at this point, just try what feels logical!
<!-- And if anything doesn't work that you think should work, please feel free to ask any clarifying questions under this video! -->

### Compiler generates code from templates in a lazy way
After you've experimented with all of this for a while, you might start noticing something...
## Compiler is lazy
After we've experimented with all of this for a while, we might start noticing something...

> 🚨 Note, and this is very important, that the compiler is **lazy**! Only those specializations are generated that are **actually used**!
> 🚨 Note, and this is very important, that the compiler is **lazy**! Only those specializations are generated that are actually used!
If we didn't use the `Maximum` function it would not have been compiled at all! Now if we think about it for a minute it becomes clear why this is so --- looking at the `Maximum` function template itself, in isolation, the compiler doesn't know which types it will be used with!
<!-- Add a travolta meme -->

If we didn't use the `Maximum` function it would not have been compiled at all! Now if we think about it for a minute it becomes clear why this is so --- looking at the `Maximum` function template itself, in isolation, the compiler doesn't know which types it will be used with! Technically, it could be any type we want! So the compiler cannot (and should not) compile the code for **all** the types it knows about. There are multiple reasons for this:
Technically, it could be any type we want! So the compiler cannot (and should not) compile the code for **all** the types it knows about. There are multiple reasons for this:
1. It would take ages --- it would need to compile **a lot** of code!
2. The binary size would be huge --- it would need to contain all that binary code!
3. It might still not be enough as maybe we will give out our template function as part of a header-only library and we simply don't know which types it might be used with!

This is actually something that plays a lead role in making splitting templated code between header and source files confusing for beginners. We'll talk about this very soon, but if we understand everything that we talked about here, we should face no issues there too, when the time comes.
This is actually something that plays a lead role in making splitting templated code between header and source files confusing for beginners. We'll talk about this very soon, but if you understand everything that we talked about here, you should face no issues there too, when the time comes.

## Summary
Ok, time for a short recap and after it we can be sure that we know the most important bits about what is it that transforms templates into actual binary code.

Long story short, the compiler generates concrete functions, classes or structs for any template instantiation whenever it encounters such instantiations in the code. It then treats these as any other ordinary function, class or struct and proceeds by compiling them into binary object files. From this point on these symbols behave just like any normal class or function and those we already know about :wink:
Long story short, the compiler generates concrete functions, classes or structs for any template instantiation whenever it encounters such instantiations in the code. It then treats these as any other ordinary function, class or struct and proceeds by compiling them into binary object files. From this point on these symbols in the object files behave just like any normal class or function and those we already know about :wink:

And that's it, conceptually, this is everything that happens under the hood when we use templates. Now, there are a lot of intricate details on which exact concrete binary code gets generated depending on how we declare and define our templates and we will cover this in the next video!

<!-- Until then, please feel free to watch the video about why we might want to use templates in the first place if you haven't already or jump to the next video about how to write templated code as soon as I record and upload it!
<!-- Until then, please feel free to watch the video about why we might want to use templates in the first place if you haven't already!
As always, thanks for watching and see you in the next one! Bye! -->
And at this, time to end this video, so, as always, thanks for watching and see you in the next one! Bye! -->

0 comments on commit 69a961b

Please sign in to comment.