Skip to content
Snippets Groups Projects
Commit 1bd58837 authored by Marcus Mohr's avatar Marcus Mohr
Browse files

Adds first notebook dealing (explicitely) with templates

parent 684e43d9
Branches
No related merge requests found
%% Cell type:markdown id:741a2490 tags:
# Template Programming: Basics
%% Cell type:markdown id:a61d218d tags:
## Motivation
In our first notebook **01_Overloading** we started with implementing two free-functions that returned the magnitude of their argument:
%% Cell type:code id:d4a7139f tags:
``` C++14
// Version for int
int getMagnitude( int input ) {
return input > 0 ? input : -input;
}
```
%% Cell type:code id:cfbe1b82 tags:
``` C++14
// Version for double
double getMagnitude( double input ) {
return input > 0.0 ? input : -input;
}
```
%% Cell type:markdown id:6f937e9d tags:
Comparison of the code shows that there are three differences between the two functions:
1. type of argument
1. type of return value
1. type of literal used
The last one can be eliminated by performing a static cast.
%% Cell type:code id:0225265d tags:
``` C++14
// Version for double
double getMagnitude( double input ) {
return input > static_cast< double >( 0 ) ? input : -input;
}
```
%% Cell type:markdown id:13e637a9 tags:
Now imagine that we want to have one function for every signed integer data type and each floating point type in C++, i.e.
- signed char
- signed short
- signed int
- signed long
- signed long long
- float
- double
- long double
This implies that we need to implement eight different versions of `getMagnitude`, which, however, are always following the same structure:
```c++
// Basic pattern of our free-function for a datatype TYPE is:
TYPE getMagnitude( TYPE input ) {
return input > static_cast< TYPE >( 0 ) ? input : -input;
}
```
%% Cell type:markdown id:3013b562 tags:
---
**Alternatives?**
It would be nice, if we did not have to write the (basically) same code multiple times
- Repetition is dull and error-prone
- We need to maintain (basically) the same code piece multiple times -> more work and error-prone
Fortunately there are ways to avoid this. The two prominent ones applicable in our situation are:
1. (Automatic) Code Generation
1. Generic Programming
%% Cell type:markdown id:6dc7ea86 tags:
### (Automatic) Code Generation
Here the idea is to let the computer generate the source code that the compiler sees itself following our instructions.
For this we need some (external) code generator. Often this is written in some other language than the generated code itself (e.g. in Python).
> An example of this would be the
<a href="https://fenicsproject.org">FEniCS</a> project, a popular open-source computing platform for solving partial differential equations with Finite Elements. You describe your PDE problem in Python and from this efficient C++ code is generated, compiled and executed (very rough description :-)
Often such generators are tuned to a specific field of problems and involve a so called
DSL (domain specific language) to describe what is to be solved/generated.
> For more on this see e.g. the publications of the <a href="https://www.exastencils.fau.de">ExaStencils</a> project.
Automatic code generation plays an increasingly important role in high-performance scientific applications for **performance tuning** and **(performance) portability** (think CPU vs. GPU vs FPGA vs ...)
%% Cell type:markdown id:3191c55f tags:
### 'Inline' Code Generation
In C++ it is possible to do some (limited) code generation on-the-fly using the capabilities of the **preprocessor**. An interesting overview on that can be found e.g. in <a href="https://link.springer.com/book/10.1007/978-3-662-48550-7">C++ Metaprogrammierung</a>, Lemke, 2016, Springer.
We are going to take a look at this as an example, before proceeding to templates.
%% Cell type:code id:988d30ea tags:
``` C++14
%%file getMag.cpp
// we use a parameterised macro that represents the pattern of our function
#define GETMAG( TYPE ) \
TYPE getMagnitude( TYPE value ) { \
return value > static_cast< TYPE >( 0 ) ? value : -value; \
}
// Now we can let the preprocessor generate the source code by using the
// macro with the corresponding datatype we need
GETMAG( int )
GETMAG( double )
GETMAG( float )
```
%% Output
Overwriting getMag.cpp
%% Cell type:markdown id:625b1b11 tags:
Now let us check how the result of running the preprocessor on that source code looks like:
%% Cell type:code id:8baeee45 tags:
``` C++14
!cpp getMag.cpp
```
%% Output
# 1 "getMag.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "getMag.cpp"
# 10 "getMag.cpp"
int getMagnitude( int value ) { return value > static_cast< int >( 0 ) ? value : -value; }
double getMagnitude( double value ) { return value > static_cast< double >( 0 ) ? value : -value; }
float getMagnitude( float value ) { return value > static_cast< float >( 0 ) ? value : -value; }
%% Cell type:markdown id:9ea5d77e tags:
---
Preprocessing is an integral part of compilation of a C++ program (think of the `#include` directive) so this integrates seamlessly:
%% Cell type:code id:b1d7de95 tags:
``` C++14
%%file getMag.cpp
// we use a parameterised macro that represents the pattern of our function
#define GETMAG( TYPE ) \
TYPE getMagnitude( TYPE value ) { \
return value < static_cast< TYPE >( 0 ) ? -value : value; \
}
// Now we can let the preprocessor generate the source code by using the
// macro with the corresponding datatype we need
GETMAG( signed char )
GETMAG( short )
GETMAG( int )
GETMAG( long )
GETMAG( float )
GETMAG( double )
#include <iostream>
int main() {
short s = -2;
float x = 3.5f;
double v = 12.34;
std::cout << "|" << s << "| = " << getMagnitude( s ) << std::endl;
std::cout << '|' << x << "| = " << getMagnitude( x ) << std::endl;
std::cout << '|' << v << "| = " << getMagnitude( v ) << std::endl;
}
```
%% Output
Overwriting getMag.cpp
%% Cell type:code id:b85f934b tags:
``` C++14
!g++ -Wall -Wextra getMag.cpp
```
%% Cell type:code id:00684f47 tags:
``` C++14
!./a.out
```
%% Output
|-2| = 2
|3.5| = 3.5
|12.34| = 12.34
%% Cell type:markdown id:6631c752 tags:
The downside of this approach is that the C/C++ preprocessor has nearly no understanding of the language itself. Hence, it cannot perform any syntax checking or the like.
Writing longer functions will become irksome, also, due to the line continuation stuff. *(and no syntax high-lighting or indentation ... by editors).*
Also we will need to **explicitely have a line for each `getMagnitude()` version** that might be used. Code will not be generated on a need-to-have basis.
%% Cell type:markdown id:dda3bd44 tags:
### Generic Programming
The idea here is to not use an external code generator, but instead provide functionality in the programming language itself that allows to accomplish what we want.
From <a href="https://en.wikipedia.org/wiki/Generic_programming">Wikipedia</a>:
> Generic programming is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters.
In C++ the approach to support generic programming is to use **templates**.
%% Cell type:markdown id:c9b5ee37 tags:
---
So how would we takle our problem with templates?
Instead of multiple free-functions we implement a **single templated version** of it:
%% Cell type:code id:90ddfa90 tags:
``` C++14
template< typename T >
T getMagnitude( T value ) {
return value > static_cast< T >( 0 ) ? value : -value;
}
```
%% Cell type:markdown id:e2df86d3 tags:
When the compiler recognises that an instance of the templated function is needed for a specific data-type it will instantiate it (i.e. it will compile machine-code for this version):
%% Cell type:code id:f5be2836 tags:
``` C++14
#include <iostream>
short s = -5;
std::cout << "|" << s << "| = " << getMagnitude( s );
```
%% Cell type:markdown id:740ee0bd tags:
---
A closer look at the **function template**:
```c++
template< typename T >
T getMagnitude( T value ) {
return value < static_cast< T >( 0 ) ? -value : value;
}
```
* `template< typename T >`
- informs the compiler that it is a templated function
- in the example there is a single **template parameter** (often denoted as `T`, but that is
just convention)
- `typename` specifies `T` to be a **type** template parameter (`class` can be used interchangeably)
- instead of a datatype a parameter can also be a basic datatype ( e.g. `template <int n>` ), or a *template template parameter*
* In the body of the template declaration `T` is a typedef-name that aliases the type supplied when the template is instantiated
%% Cell type:markdown id:d6754844-ef2d-496e-adab-1367a463c19f tags:
---
Now let's take a closer look at the **instantiation**:
%% Cell type:code id:8f2ab60c tags:
``` C++14
%%file withTemplates.cpp
template< typename T >
T getMagnitude( T value ) {
return value > static_cast< T >( 0 ) ? value : -value;
}
int main() {
short s = -2;
float x = 3.5f;
double v = 12.34;
getMagnitude( s );
getMagnitude( x );
getMagnitude( v );
getMagnitude( -10L );
}
```
%% Cell type:code id:96551727-c1b0-4b97-9bff-11e9eeac731e tags:
``` C++14
!g++ -save-temps -c withTemplates.cpp
```
%% Cell type:code id:d3e0f1fc-f85a-4adc-a63f-ba7fdea02a2d tags:
``` C++14
!nm -C withTemplates.o
```
%% Cell type:markdown id:d63382c0-e766-422b-ba97-7824de507c3a tags:
We observe that the object file contains instantiations of all four versions that are required in our source code, *but not more*. This is called **implicit instantiation**.
A look at the file after preprocessing (`-save-temps` option) shows that no extra source code got generated. It really is something the compiler handles for us.
%% Cell type:code id:9c620720-b4cc-46a0-bde0-57452bcd3a0b tags:
``` C++14
!cat withTemplates.ii
```
%% Cell type:code id:38aa85e7-83d9-4ae0-ac05-80cf187ee9f3 tags:
``` C++14
%%file withTemplates.cpp
template< typename T >
T getMagnitude( T value ) {
return value > static_cast< T >( 0 ) ? value : -value;
}
// explicit instantiation
template float getMagnitude( float );
int main() {
// implicit instantiation
getMagnitude( -2 );
}
```
%% Cell type:code id:76bfe507-bc01-457a-aeb5-38e2db572cac tags:
``` C++14
!g++ -save-temps -c withTemplates.cpp
```
%% Cell type:code id:e707d5e2-6d8d-4ee9-9046-b749d154146b tags:
``` C++14
!nm -C withTemplates.o
```
%% Cell type:markdown id:76ecc704-ac09-4343-b561-7817373aa1c9 tags:
When might explicit instantiation be required?
Let us modify our example to have the usual structure of *header file* (.hpp) and *source code file* (.cpp) and a separate *driver file*:
%% Cell type:code id:a854c3ce-1865-4ec1-b092-87be2a50d13f tags:
``` C++14
%%file withTemplates.cpp
template< typename T >
T getMagnitude( T value ) {
return value > static_cast< T >( 0 ) ? value : -value;
}
```
%% Cell type:code id:b3b433db-8509-43fb-9946-579ffd4c5dfb tags:
``` C++14
%%file withTemplates.hpp
template< typename T > T getMagnitude( T value );
```
%% Cell type:code id:c96bbc17-3771-453b-9544-eae96dd936f0 tags:
``` C++14
%%file driver.cpp
#include "withTemplates.hpp"
int main() {
getMagnitude( -1.2345f );
}
```
%% Cell type:code id:0cea5aa2-240b-493c-8afc-c560b0dff82c tags:
``` C++14
!g++ driver.cpp
```
%% Cell type:markdown id:976e6ef4-ce58-4a4f-acf9-bff9d122923d tags:
Okay, that is not different from the usual. Let us also compile the source code file:
%% Cell type:code id:e98425fb-dc85-4e89-9f9b-dc310b1c65bb tags:
``` C++14
!g++ driver.cpp withTemplates.cpp
```
%% Cell type:markdown id:0ace12fa-d5a8-48ab-8d9b-250c71847812 tags:
Still a problem! Why's that?
%% Cell type:markdown id:bf20a5c9-5d48-405f-861e-afc7d699ff41 tags:
*intentionally left blank ;-)*
%% Cell type:markdown id:1517f452-ef0e-471a-82c0-880e829a51ff tags:
When we compile the driver the compiler will recognise that it need a version of `getMagnitude()` for `float`:
%% Cell type:code id:846d3074-53c4-4dfe-94af-43b82f49d0a8 tags:
``` C++14
!g++ -c driver.cpp
```
%% Cell type:code id:7f84b616-97d9-4ce0-a588-7bed26c4b4aa tags:
``` C++14
!nm -C driver.o
```
%% Cell type:markdown id:43c992a6-7dca-4aab-8293-328b0cd8bf76 tags:
However, it cannot instantiate it, because it does not see the definition of the template function, but only its prototype!
- That is no error.
- Finding an instance is then delegated to the linker.
Okay. So now let us compile the source code file and check the contents of the resulting object file:
%% Cell type:code id:15c7f98e-33fd-4bca-8293-5a19352c43ac tags:
``` C++14
!g++ -c withTemplates.cpp
```
%% Cell type:code id:5f6a4521-affc-4147-a33a-a7fd4f6051ce tags:
``` C++14
!nm -C withTemplates.o
```
%% Cell type:markdown id:b955ee26-e340-44e5-a264-66beda062321 tags:
Oops. Nothing. Why?
Because in the source code file no template instance is required and the template function itself cannot be compiled with out an argument for the datatype!
%% Cell type:markdown id:6bd45ab9-c3b9-4ec2-b093-1a2f327a0526 tags:
---
Splitting declaration and definition of a template function requires us to use explicit instantiation.
%% Cell type:code id:e0aa1181-d4b2-4fc1-95fe-ead0f86b2d5b tags:
``` C++14
%%file withTemplates.cpp
template< typename T >
T getMagnitude( T value ) {
return value > static_cast< T >( 0 ) ? value : -value;
}
template float getMagnitude( float );
```
%% Cell type:code id:e86d0de4-3e7e-48d1-8b2f-3a2b29e44842 tags:
``` C++14
!g++ -save-temps driver.cpp withTemplates.cpp
```
%% Cell type:code id:380b4117-4a81-45dc-a578-88f233d8726e tags:
``` C++14
!nm -C withTemplates.o
```
%% Cell type:code id:68c3df86-8d83-4b24-bb13-8bb90e170eb2 tags:
``` C++14
```
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment