5CCYB041
We continue working on our fMRI analysis project
You can find the most up to date version in the project's solution/
folder
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);
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
float
?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
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);
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
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!
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;}
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
double
?C++ provides a way to define a function for one or more generic types
C++ provides a way to define a function for one or more generic types
We have already been using template functions throughout this course:
std::min()
, std::max()
, std::ranges::min()
, std::ranges::max()
,
std::distance()
, std::format()
, ...C++ provides a way to define a function for one or more generic types
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;}
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<double> task; ...auto task_rescaled = rescale (task, 0.0, 1000.0);
When the compiler encounters this line:
auto task_rescaled = rescale (task, 0.0, 1000.0);
It will look up the types of all the arguments, and note that they are:
task
: std::vector<double>
0.0
: double
1000.0
: double
When the compiler encounters this line:
auto task_rescaled = rescale (task, 0.0, 1000.0);
It will look up the types of all the arguments, and note that they are:
task
: std::vector<double>
0.0
: double
1000.0
: double
This matches the previously-declared template function, since T
can be
substituted for double
:
template <typename T>std::vector<float> rescale (const std::vector<T>& task, T min, T max);
When the compiler encounters this line:
auto task_rescaled = rescale (task, 0.0, 1000.0);
It will look up the types of all the arguments, and note that they are:
task
: std::vector<double>
0.0
: double
1000.0
: double
This matches the previously-declared template function, since T
can be
substituted for double
:
template <typename T>std::vector<float> rescale (const std::vector<T>& task, T min, T max);
The compiler can now actually compile the function:
std::vector<float> rescale (const std::vector<double>& task, double min, double max);
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
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
The type will only be known at the point where the function is used
rescale()
is declated in task.h
, but used in fmri.cpp
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
The type will only be known at the point where the function is used
rescale()
is declated in task.h
, but used in fmri.cpp
The compiler can only produce the required code if it has access to the full definition
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
The type will only be known at the point where the function is used
rescale()
is declated in task.h
, but used in fmri.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
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
The type will only be known at the point where the function is used
rescale()
is declated in task.h
, but used in fmri.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
The simplest approach is to place the full definition in the header file, alongside the declaration
.cpp
fileTo 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);
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;}
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
:
rescale()
Then when compiling task.cpp
:
rescale()
When compiling fmri.cpp
:
rescale<int>()
version rescale<int>()
must have been produced elsewheretask.o
would mention that he function rescale<int>()
is
being usedThen when compiling task.cpp
:
rescale()
When compiling fmri.cpp
:
rescale<int>()
version rescale<int>()
must have been produced elsewheretask.o
would mention that he function rescale<int>()
is
being usedWhen linking fmri.o
, task.o
, etc:
rescale<int>()
Then when compiling task.cpp
:
rescale()
When compiling fmri.cpp
:
rescale<int>()
version rescale<int>()
must have been produced elsewheretask.o
would mention that he function rescale<int>()
is
being usedWhen linking fmri.o
, task.o
, etc:
rescale<int>()
For this reason, the definition of a template function should be included in the header file!
.cpp
fileinline
to prevent the multiple
symbol problem when linking Then when compiling task.cpp
:
rescale()
When compiling fmri.cpp
:
rescale<int>()
version rescale<int>()
must have been produced elsewheretask.o
would mention that he function rescale<int>()
is
being usedWhen linking fmri.o
, task.o
, etc:
rescale<int>()
For this reason, the definition of a template function should be included in the header file!
.cpp
fileinline
to prevent the multiple
symbol problem when linking Exercise: convert the rescale()
function to a template function
There can be more than a single template argument
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];}
Note that issues can occur due to template argument deduction, when the types provided don't match where they should.
For example, there can be issue with our rescale()
function when the type deduced
by the compiler for the min
& max
limits doesn't match the type for the
elements of the vector.
Consider this example:
std::vector<short unsigned int> task; ...auto task_rescaled = rescale (task, 0, 1000);
This will produced an error because the arguments 0
& 1000
are deduced as being
of type int
, but the type of the vector elements is declared as short
unsigned int
T
)T
!The full compiler error is shown on the next slide
fmri.cpp: In function ‘void run(std::vector<std::__cxx11::basic_string<char> >&)’:fmri.cpp:67:23: error: no matching function for call to ‘rescale(std::vector<short unsigned int>&, int, int)’ 67 | auto task_rescaled = rescale (task, 0, 1000); | ~~~~~~~~^~~~~~~~~~~~~~In file included from fmri.cpp:13:task.h:9:20: note: candidate: ‘template<class ValueType> std::vector<float> rescale(const std::vector<ValueType>&, ValueType, ValueType)’ 9 | std::vector<float> rescale (const std::vector<ValueType>& task, ValueType min, ValueType max) | ^~~~~~~task.h:9:20: note: template argument deduction/substitution failed:fmri.cpp:67:23: note: deduced conflicting types for parameter ‘ValueType’ (‘short unsigned int’ and ‘int’) 67 | auto task_rescaled = rescale (task, 0, 1000); | ~~~~~~~~^~~~~~~~~~~~~~
A simple solution to this problem is to allow the types for the min
& max
arguments to differ from the type of the vector elements.
rescale()
function by adding at least one more template
parameter to its declaration, to avoid the issue mentioned in the previous
slides.template <typename T, typename C>std::vector<float> rescale (const std::vector<T>& task, C min, C 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;}
Over the last few sessions, we have come up with a useful Image
class
Over the last few sessions, we have come up with a useful Image
class
As written, our Image
class is hard-coded to use int
to store the intensity
of each pixel
short unsigned int
) to
limit memory usage?Over the last few sessions, we have come up with a useful Image
class
As written, our Image
class is hard-coded to use int
to store the intensity
of each pixel
short unsigned int
) to
limit memory usage?We could:
Image
classImageFloat
insteadint
to float
where relevantOver the last few sessions, we have come up with a useful Image
class
As written, our Image
class is hard-coded to use int
to store the intensity
of each pixel
short unsigned int
) to
limit memory usage?We could:
Image
classImageFloat
insteadint
to float
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_dim { xdim, ydim }, m_data (xdim*ydim, 0) { } Image (int xdim, int ydim, const std::vector<int>& data) : m_dim { xdim, ydim }, m_data (data) { if (static_cast<int> (m_data.size()) != m_dim[0] * m_dim[1]) throw std::runtime_error ("dimensions don't match"); } int width () const { return m_dim[0]; } int height () const { return m_dim[1]; } const int& operator() (int x, int y) const { return m_data[x + m_dim[0]*y]; } int& operator() (int x, int y) { return m_data[x + m_dim[0]*y]; } private: std::array<int,2> m_dim; std::vector<int> m_data;};
In image.h
:
class Image { public: Image (int xdim, int ydim) : m_dim { xdim, ydim }, m_data (xdim*ydim, 0) { } Image (int xdim, int ydim, const std::vector<int>& data) : m_dim { xdim, ydim }, m_data (data) { if (static_cast<int> (m_data.size()) != m_dim[0] * m_dim[1]) throw std::runtime_error ("dimensions don't match"); } int width () const { return m_dim[0]; } int height () const { return m_dim[1]; } const int& operator() (int x, int y) const { return m_data[x + m_dim[0]*y]; } int& operator() (int x, int y) { return m_data[x + m_dim[0]*y]; } private: std::array<int,2> m_dim; std::vector<int> m_data;};
Currently, our Image
class is hard-coded to store int
values
In image.h
:
class Image { public: Image (int xdim, int ydim) : m_dim { xdim, ydim }, m_data (xdim*ydim, 0) { } Image (int xdim, int ydim, const std::vector<float>& data) : m_dim { xdim, ydim }, m_data (data) { if (static_cast<int> (m_data.size()) != m_dim[0] * m_dim[1]) throw std::runtime_error ("dimensions don't match"); } int width () const { return m_dim[0]; } int height () const { return m_dim[1]; } const float& operator() (int x, int y) const { return m_data[x + m_dim[0]*y]; } float& operator() (int x, int y) { return m_data[x + m_dim[0]*y]; } private: std::array<int,2> m_dim; std::vector<float> m_data;};
... 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_dim { xdim, ydim }, m_data (xdim*ydim, 0) { } Image (int xdim, int ydim, const std::vector<T>& data) : m_dim { xdim, ydim }, m_data (data) { if (static_cast<int> (m_data.size()) != m_dim[0] * m_dim[1]) throw std::runtime_error ("dimensions don't match"); } int width () const { return m_dim[0]; } int height () const { return m_dim[1]; } const T& operator() (int x, int y) const { return m_data[x + m_dim[0]*y]; } T& operator() (int x, int y) { return m_data[x + m_dim[0]*y]; } private: std::array<int,2> m_dim; std::vector<T> m_data;};
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
T
in our definitionIn image.h
:
template <typename T>class Image { public: Image (int xdim, int ydim) : m_dim { xdim, ydim }, m_data (xdim*ydim, 0) { } Image (int xdim, int ydim, const std::vector<T>& data) : m_dim { xdim, ydim }, m_data (data) { if (static_cast<int> (m_data.size()) != m_dim[0] * m_dim[1]) throw std::runtime_error ("dimensions don't match"); } int width () const { return m_dim[0]; } int height () const { return m_dim[1]; } const T& operator() (int x, int y) const { return m_data[x + m_dim[0]*y]; } T& operator() (int x, int y) { return m_data[x + m_dim[0]*y]; } private: std::array<int,2> m_dim; std::vector<T> m_data;};
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_dim { xdim, ydim }, m_data (xdim*ydim, 0) { } Image (int xdim, int ydim, const std::vector<T>& data) : m_dim { xdim, ydim }, m_data (data) { if (static_cast<int> (m_data.size()) != m_dim[0] * m_dim[1]) throw std::runtime_error ("dimensions don't match"); } int width () const { return m_dim[0]; } int height () const { return m_dim[1]; } const T& operator() (int x, int y) const { return m_data[x + m_dim[0]*y]; } T& operator() (int x, int y) { return m_data[x + m_dim[0]*y]; } private: std::array<int,2> m_dim; std::vector<T> m_data;};
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);
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
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
You'll note that we have already been using class templates since the beginning!
std::vector
is a class templateAs with template functions, the compiler will not produce any code when encountering a template class definition
As with template functions, the compiler will not produce any code when encountering a template class definition
The compiler will only generate code when the data type is known: at the point of use
As with template functions, the compiler will not produce any code when encountering a template class definition
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!
inline
to prevent the multiple
symbol problem when linking Convert the Image
class to a template class, where the template parameter
determines the type to use for the image intensities. Modify the rest of the
code to make use of it
Convert the Dataset
class to a template class, where the template parameter
determines the type to use for the image intensities. 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
:
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
std::vector
class has a well-defined interface, but
different implementations for different typesPolymorphism is one of the key feature of Object-Oriented Programming
It refers to ability to define a single interface with multiple implementations
std::vector
class has a well-defined interface, but
different implementations for different typesC++ provides several mechanisms to implement polymorphism:
Polymorphism is one of the key feature of Object-Oriented Programming
It refers to ability to define a single interface with multiple implementations
std::vector
class has a well-defined interface, but
different implementations for different typesC++ provides several mechanisms to implement polymorphism:
Polymorphism is one of the key feature of Object-Oriented Programming
It refers to ability to define a single interface with multiple implementations
std::vector
class has a well-defined interface, but
different implementations for different typesC++ provides several mechanisms to implement polymorphism:
Our template Image
class is an example of compile-time polymorphism
rescale()
functionsrescale()
functionConvert the Image
class to a template class, where the template parameter
determines the type to use for the image intensities. Modify the rest of the
code to make use of it
Convert the Dataset
class to a template class, where the template parameter
determines the type to use for the image intensities. Modify the rest of the
code to make use of it
We continue working on our fMRI analysis project
You can find the most up to date version in the project's solution/
folder
Make sure your code is up to date now!
Keyboard shortcuts
↑, ←, Pg Up, k | Go to previous slide |
↓, →, Pg Dn, Space, j | Go to next slide |
Home | Go to first slide |
End | Go to last slide |
Number + Return | Go to specific slide |
b / m / f | Toggle blackout / mirrored / fullscreen mode |
c | Clone slideshow |
p | Toggle presenter mode |
s | Start & Stop the presentation timer |
t | Reset the presentation timer |
?, h | Toggle this help |
Esc | Back to slideshow |