layout | title |
---|---|
presentation |
Week 5, session 1: template programming, lambda expressions |
class: title
5CCYB041
We continue working on our fMRI analysis project
You can find the most up to date version in the project's solution/
folder
.explain-bottom[ Make sure your code is up to date now! ]
Our rescale function is currently declared in task.h
as:
std::vector<float> rescale (const std::vector<int>& task, int min, int max);
--
Currently, this can only be used to rescale a vector of int
- what if we wanted to use the same functionality to rescale a vector of
float
?
-- We could use function overloading:
std::vector<float> rescale (const std::vector<`int`>& task, `int` min, `int` max);
std::vector<float> rescale (const std::vector<`float`>& task, `float` min, `float` max);
--
... but then we would have to duplicate the function definition too!
In task.cpp
:
std::vector<float> rescale (const std::vector<int>& task, int min, int max)
{
std::vector<float> out (task.size());
for (unsigned int n = 0; n < task.size(); ++n)
out[n] = min + task[n] * (max-min);
return out;
}
std::vector<float> rescale (const std::vector<float>& task, float min, float max)
{
std::vector<float> out (task.size());
for (unsigned int n = 0; n < task.size(); ++n)
out[n] = min + task[n] * (max-min);
return out;
}
Note that the functions are identical other than their declaration!
--
Function overloading is useful, but may not be the best tool here
- what if at some later stage, we need to do the same thing with a vector of
double
? - what if we find a subtle bug in the code? We would need to correct the error in all the different versions
class: section
C++ provides a way to define a function for one or more generic types
- this allows us to provide a blueprint for what the function should do
- when we need to use the function, the compiler will work out what the type is, substitute it into our code, and compile that
- this allows us to write a generic function once and re-use it in different
contexts
- this is a great tool to maximise code re-use
--
We have already been using template functions throughout this course:
std::min()
,std::max()
,std::ranges::min()
,std::ranges::max()
,std::distance()
,std::format()
, ...
--
Let's illustrate the concept by modifying our task rescale()
function
Writing a template function avoids the need for multiple overloaded functions by allowing us to provide a single generic definition:
`template <typename T>`
std::vector<float> rescale (const std::vector<`T`>& task, `T` min, `T` max)
{
std::vector<float> out (task.size());
for (unsigned int n = 0; n < task.size(); ++n)
out[n] = min + task[n] * (max-min);
return out;
}
--
We can then use the function as before, for any type (within reason...):
std::vector<`unsigned short int`> task;
...
auto task_rescaled = rescale (task, 0, 1000);
When using templates, there are a few issues to watch out for:
The compiler won't actually produce any code until the type is known
- all it can do when encountering the definition is make sure the syntax is correct
--
The type will only be known at the point where the function is used
- this is normally in a different file from the declaration
- for example,
rescale()
is declated intask.h
, but used infmri.cpp
--
The compiler can only produce the required code if it has access to the full definition
--
⇒ the definition must be available in the same translation unit
--
We have to place the full definition in the header file, alongside the declaration
- there is no point including it in the corresponding
.cpp
file- the compiler won't generate any code until the type is specified
- this is different from regular functions
To illustrate the problem, imagine we declare our template function in task.h
:
template <typename T>
std::vector<float> rescale (const std::vector<T>& task, T min, T max);
--
... define it in task.cpp
:
template <typename T>
std::vector<float> rescale (const std::vector<`T`>& task, `T` min, `T` max)
{
std::vector<float> out (task.size());
for (unsigned int n = 0; n < task.size(); ++n)
out[n] = min + task[n] * (max-min);
return out;
}
--
... and use it in fmri.cpp
:
std::vector<int> task;
...
auto task_rescaled = rescale (task, 0, 1000);
Then when compiling task.cpp
:
- no code would be produced for any version of
rescale()
- the compiler doesn't know which type might be requested
--
When compiling fmri.cpp
:
- no definition would be available for the desired
rescale<int>()
version - the compiler would nonetheless assume that a version of
rescale<int>()
must have been produced elsewhere - the output file
task.o
would mention that he functionrescale<int>()
is being used
--
When linking fmri.o
, task.o
, etc:
- we will have an unresolved symbol error: there is no function
rescale<int>()
--
For this reason, **the definition of a template function must be included in the header file**! - *not* in the `.cpp` file - the function will implicitly be marked `inline` to prevent the multiple symbol problem when linking
--
.explain-bottom[
Exercise: convert the rescale()
function to a template function
]
There can be more than a single template argument
--
For example, this function allows one vector to be added to another, with the results stored in-place in the first vector – even if the types differ:
template <`typename A, typename B`>
void add_in_place (std::vector<`A`>& vecA, const std::vector<`B`>& vecB)
{
if (vecA.size() != vecB.size())
throw std::runtime_error ("vector size must be the same");
for (unsigned int n = 0; n < vecA.size(); ++n)
vecA[n] += vecB[n];
}
class: section
Over the last few sessions, we have come up with a useful Image
class
- but it has its limitations
--
As written, our Image
class is hard-coded to use int
to store the intensity
of each pixel
- what if we wanted to use a smaller type (e.g. a 16-bit
unsigned int
) to limit memory usage? - what if we needed to store floating-point values?
--
We could:
- copy/paste our whole
Image
class - call it
ImageFloat
instead - change
int
tofloat
where relevant
--
... but that is a lot of code duplication!
⇒ let's use C++ templates instead
In image.h
:
class Image {
public:
Image (int xdim, int ydim) :
m_xdim (xdim), m_ydim (ydim), m_data (xdim*ydim, 0) { }
Image (int xdim, int ydim, const std::vector<int>& data) :
m_xdim (xdim), m_ydim (ydim), m_data (data) {
if (static_cast<int> (m_data.size()) != m_xdim * m_ydim)
throw std::runtime_error ("dimensions don't match");
}
int width () const { return m_xdim; }
int height () const { return m_ydim; }
const int& operator() (int x, int y) const { return m_data[x + m_xdim*y]; }
int& operator() (int x, int y) { return m_data[x + m_xdim*y]; }
private:
int m_xdim, m_ydim;
std::vector<int> m_data;
};
In image.h
:
class Image {
public:
Image (int xdim, int ydim) :
m_xdim (xdim), m_ydim (ydim), m_data (xdim*ydim, 0) { }
Image (int xdim, int ydim, const std::vector<`int`>& data) :
m_xdim (xdim), m_ydim (ydim), m_data (data) {
if (static_cast<int> (m_data.size()) != m_xdim * m_ydim)
throw std::runtime_error ("dimensions don't match");
}
int width () const { return m_xdim; }
int height () const { return m_ydim; }
const `int`& operator() (int x, int y) const { return m_data[x + m_xdim*y]; }
`int`& operator() (int x, int y) { return m_data[x + m_xdim*y]; }
private:
int m_xdim, m_ydim;
std::vector<`int`> m_data;
};
.explain-topright[
Currently, our Image
class is hard-coded to store int
values
]
In image.h
:
class Image {
public:
Image (int xdim, int ydim) :
m_xdim (xdim), m_ydim (ydim), m_data (xdim*ydim, 0) { }
Image (int xdim, int ydim, const std::vector<`float`>& data) :
m_xdim (xdim), m_ydim (ydim), m_data (data) {
if (static_cast<int> (m_data.size()) != m_xdim * m_ydim)
throw std::runtime_error ("dimensions don't match");
}
int width () const { return m_xdim; }
int height () const { return m_ydim; }
const `float`& operator() (int x, int y) const { return m_data[x + m_xdim*y]; }
`float`& operator() (int x, int y) { return m_data[x + m_xdim*y]; }
private:
int m_xdim, m_ydim;
std::vector<`float`> m_data;
};
.explain-topright[
... but if we changed the type from int
to float
at the locations
highlighted, this class would work just as well for float
!
]
In image.h
:
`template <typename T>`
class Image {
public:
Image (int xdim, int ydim) :
m_xdim (xdim), m_ydim (ydim), m_data (xdim*ydim, 0) { }
Image (int xdim, int ydim, const std::vector<T>& data) :
m_xdim (xdim), m_ydim (ydim), m_data (data) {
if (static_cast<int> (m_data.size()) != m_xdim * m_ydim)
throw std::runtime_error ("dimensions don't match");
}
int width () const { return m_xdim; }
int height () const { return m_ydim; }
const T& operator() (int x, int y) const { return m_data[x + m_xdim*y]; }
T& operator() (int x, int y) { return m_data[x + m_xdim*y]; }
private:
int m_xdim, m_ydim;
std::vector<T> m_data;
};
.explain-topright[
Our Image
class can readily be converted into a template
class
As with template functions, we need to use the template
keyword to denote
the class as a template, and list the arguments this template will take
- our template accepts a single type argument, which will be referred to as
T
in our definition ]
In image.h
:
template <typename T>
class Image {
public:
Image (int xdim, int ydim) :
m_xdim (xdim), m_ydim (ydim), m_data (xdim*ydim, 0) { }
Image (int xdim, int ydim, const std::vector<`T`>& data) :
m_xdim (xdim), m_ydim (ydim), m_data (data) {
if (static_cast<int> (m_data.size()) != m_xdim * m_ydim)
throw std::runtime_error ("dimensions don't match");
}
int width () const { return m_xdim; }
int height () const { return m_ydim; }
const `T`& operator() (int x, int y) const { return m_data[x + m_xdim*y]; }
`T`& operator() (int x, int y) { return m_data[x + m_xdim*y]; }
private:
int m_xdim, m_ydim;
std::vector<`T`> m_data;
};
.explain-topright[
We can now use the (as yet unknown) type T
where required instead of int
]
In image.h
:
template <typename T>
class Image {
public:
Image (`int` xdim, `int` ydim) :
m_xdim (xdim), m_ydim (ydim), m_data (xdim*ydim, 0) { }
Image (`int` xdim, `int` ydim, const std::vector<T>& data) :
m_xdim (xdim), m_ydim (ydim), m_data (data) {
if (static_cast<`int`> (m_data.size()) != m_xdim * m_ydim)
throw std::runtime_error ("dimensions don't match");
}
`int` width () const { return m_xdim; }
`int` height () const { return m_ydim; }
const T& operator() (`int` x, `int` y) const { return m_data[x + m_xdim*y]; }
T& operator() (`int` x, `int` y) { return m_data[x + m_xdim*y]; }
private:
`int` m_xdim, m_ydim;
std::vector<T> m_data;
};
.explain-topright[
Note: we don't blindly replace every mention of int
from the class!
We only replace it where it relates to its use as the pixel intensity
]
We can now declare instances of our class template for any desired type like this:
Image<int> image (256, 256);
or from an existing vector of data (of matching type):
std::vector<float> data;
...
Image<float> image (512, 512, data);
--
The compiler will then substitute the desired type (int
or float
) instead
of T
in the class definition, and compile the result
- as if we had manually substituted the type ourselves!
As with template functions, the compiler will not produce any code when encountering a template class definition
- it can only check our definition for correctness
--
The compiler will only generate code when the data type is known: at the point of use
--
⇒ the full class declaration and all method definitions must be available in the header!
- as before, all methods are implicitly marked
inline
to prevent the multiple symbol problem when linking
--
.explain-bottom[
Exercise: convert the Image
class to a template class, and modify the rest of
the code to make use of it
]
In image.h
:
`template <typename T>`
class Image {
public:
Image (int xdim, int ydim) :
m_xdim (xdim), m_ydim (ydim), m_data (xdim*ydim, 0) { }
Image (int xdim, int ydim, const std::vector<`T`>& data) :
m_xdim (xdim), m_ydim (ydim), m_data (data) {
if (static_cast<int> (m_data.size()) != m_xdim * m_ydim)
throw std::runtime_error ("dimensions don't match");
}
int width () const { return m_xdim; }
int height () const { return m_ydim; }
const `T`& operator() (int x, int y) const { return m_data[x + m_xdim*y]; }
`T`& operator() (int x, int y) { return m_data[x + m_xdim*y]; }
private:
int m_xdim, m_ydim;
std::vector<`T`> m_data;
};
Also in image.h
:
- note that the insertion operator must now also be a template
`template <class ValueType>`
inline std::ostream& operator<< (std::ostream& out, const `Image<ValueType>`& im)
In load_pgm.h/cpp
:
`Image<int>` load_pgm (const std::string& filename);
In dataset.h
:
class Dataset
{
...
const `Image<int>`& operator[] (int n) const { return m_slices[n]; }
...
private:
std::vector<`Image<int>`> m_slices;
};
Polymorphism is one of the key feature of Object-Oriented Programming
It refers to ability to define a single interface with multiple implementations
- For example, the
std::vector
class has a well-defined interface, but different implementations for different types
--
C++ provides several mechanisms to implement polymorphism:
- Compile-time polymorphim
- function overloading
- templates
--
- Run-time polymorphism
- inheritance (to be covered later)
--
Our template Image
class is an example of compile-time polymorphism
- and so were our overloaded
rescale()
functions - ... and so is our templated
rescale()
function