COMP6771
Advanced C++ Programming
Week 7.1
Templates Intro
Polymorphism & Generic Programming
- Polymorphism: Provision of a single interface to entities of different types
-
Two types - :
-
Static (our focus):
- Function overloading
-
Templates (i.e. generic programming)
- std::vector<int>
- std::vector<double>
-
Dynamic:
- Related to virtual functions and inheritance - see week 8
-
Static (our focus):
-
Genering Programming: Generalising software components to be independent of a particular type
- STL is a great example of generic programming
Function Templates
Without generic programming, to create two logically identical functions that behave in a way that is independent to the type, we have to rely on function overloading.
#include <iostream>
auto min(int a, int b) -> int {
return a < b ? a : b;
}
auto min(double a, double b) -> double{
return a < b ? a : b;
}
auto main() -> int {
std::cout << min(1, 2) << "\n"; // calls line 1
std::cout << min(1.0, 2.0) << "\n"; // calls line 4
}
Explore how this looks in Compiler Explorer
demo701-functemp1.cpp
Function Templates
-
Function template: Prescription (i.e. instruction) for the compiler to generate particular instances of a function varying by type
- The generation of a templated function for a particular type T only happens when a call to that function is seen during compile time
#include <iostream>
template <typename T>
auto min(T a, T b) -> T {
return a < b ? a : b;
}
auto main() -> int {
std::cout << min(1, 2) << "\n"; // calls int min(int, int)
std::cout << min(1.0, 2.0) << "\n"; // calls double min(double, double)
}
Explore how this looks in Compiler Explorer
demo702-functemp2.cpp
Some Terminology
template type parameter
template <typename T>
T min(T a, T b) {
return a < b ? a : b;
}
template parameter list
Type and Nontype Parameters
- Type parameter: Unknown type with no value
- Nontype parameter: Known type with unknown value
#include <array>
#include <iostream>
template<typename T, std::size_t size>
auto findmin(const std::array<T, size> a) -> T {
T min = a[0];
for (std::size_t i = 1; i < size; ++i) {
if (a[i] < min)
min = a[i];
}
return min;
}
auto main() -> int {
std::array<int, 3> x{3, 1, 2};
std::array<double, 4> y{3.3, 1.1, 2.2, 4.4};
std::cout << "min of x = " << findmin(x) << "\n";
std::cout << "min of x = " << findmin(y) << "\n";
}
Compiler deduces T and size from a
demo703-nontype1.cpp
Type and Nontype Parameters
- The above example generates the following functions at compile time
- What is "code explosion"? Why do we have to be weary of it?
auto findmin(const std::array<int, 3> a) -> int {
int min = a[0];
for (int i = 1; i < 3; ++i) {
if (a[i] < min)
min = a[i];
}
return min;
}
auto findmin(const std::array<double, 4> a) -> double {
double min = a[0];
for (int i = 1; i < 4; ++i) {
if (a[i] < min)
min = a[i];
}
return min;
}
demo704-nontype2.cpp
Class Templates
- How we would currently make a Stack type
-
Issues?
- Administrative nightmare
- Lexical complexity (need to learn all type names)
class int_stack {
public:
auto push(int&) -> void;
auto pop() -> void;
auto top() -> int&;
auto top() const -> const int&;
private:
std::vector<int> stack_;
};
class double_stack {
public:
auto push(double&) -> void;
auto pop() -> void;
auto top() -> double&;
auto top() const -> const double&;
private:
std::vector<double> stack_;
};
Class Templates
- Creating our first class template
// stack.h
#ifndef STACK_H
#define STACK_H
#include <iostream>
#include <vector>
template<typename T>
class stack {
public:
friend auto operator<<(std::ostream& os, const stack& s) -> std::ostream& {
for (const auto& i : s.stack_)
os << i << " ";
return os;
}
auto push(T const& item) -> void;
auto pop() -> void;
auto top() -> T&;
auto top() const -> const T&;
auto empty() const -> bool;
private:
std::vector<T> stack_;
};
#include "./demo705-classtemp.tpp"
#endif // STACK_H
#include "./demo705-classtemp.h"
template<typename T>
auto stack<T>::push(T const& item) -> void {
stack_.push_back(item);
}
template<typename T>
auto stack<T>::pop() -> void {
stack_.pop_back();
}
template<typename T>
auto stack<T>::top() -> T& {
return stack_.back();
}
template<typename T>
auto stack<T>::top() const -> T const& {
return stack_.back();
}
template<typename T>
auto stack<T>::empty() const -> bool {
return stack_.empty();
}
demo705-classtemp-main.h
demo705-classtemp-main.tpp
Class Templates
#include <iostream>
#include <string>
#include "./demo705-classtemp.h"
int main() {
stack<int> s1; // int: template argument
s1.push(1);
s1.push(2);
stack<int> s2 = s1;
std::cout << s1 << s2 << '\n';
s1.pop();
s1.push(3);
std::cout << s1 << s2 << '\n';
// s1.push("hello"); // Fails to compile.
stack<std::string> string_stack;
string_stack.push("hello");
// string_stack.push(1); // Fails to compile.
}
demo705-classtemp-main.cpp
Class Templates
template <typename T>
stack<T>::stack() { }
template <typename T>
stack<T>::stack(const stack<T> &s) : stack_{s.stack_} { }
template <typename T>
stack<T>::stack(Stack<T> &&s) : stack_(std::move(s.stack_)); { }
template <typename T>
stack<T>& stack<T>::operator=(const stack<T> &s) {
stack_ = s.stack_;
}
template <typename T>
stack<T>& stack<T>::operator=(stack<T> &&s) {
stack_ = std::move(s.stack_);
}
template <typename T>
stack<T>::~stack() { }
Default rule-of-five (you don't have to implement these in this case)
Inclusion compilation model
- What is wrong with this?
- g++ min.cpp main.cpp -o main
template <typename T>
auto min(T a, T b) -> T;
template <typename T>
auto min(T a, T b) -> int {
return a < b ? a : b;
}
#include <iostream>
auto main() -> int {
std::cout << min(1, 2) << "\n";
}
min.h
min.cpp
main.cpp
Inclusion compilation model
-
When it comes to templates, we include definitions (i.e. implementation) in the .h file
- This is because template definitions need to be known at compile time (template definitions can't be instantiated at link time because that would require an instantiation for all types)
- Will expose implementation details in the .h file
- Can cause slowdown in compilation as every file using min.h will have to instantiate the template, then it's up the linker to ensure there is only 1 instantiation.
template <typename T>
auto min(T a, T b) -> T {
return a < b ? a : b;
}
#include <iostream>
auto main() -> int {
std::cout << min(1, 2) << "\n";
}
min.h
main.cpp
Inclusion Compilation Model
- Alternative: Explicit instantiations
- Generally a bad idea
template <typename T>
T min(T a, T b);
template <typename T>
auto min(T a, T b) -> T {
return a < b ? a : b;
}
template int min<int>(int, int);
template double min<double>(double, double);
#include <iostream>
auto main() -> int {
std::cout << min(1, 2) << "\n";
std::cout << min(1.0, 2.0) << "\n";
}
min.h
min.cpp
main.cpp
Inclusion Compilation Model
-
Lazy instantiation: Only members functions that are called are instantiated
- In this case, pop() will not be instantiated
- Exact same principles will apply for classes
- Implementations must be in header file, and compiler should only behave as if one Stack<int> was instantiated
#include <vector>
template <typename T>
class stack {
public:
stack() {}
auto pop() -> void;
auto push(const T& i) -> void;
private:
std::vector<T> items_;
}
#include "stack.tpp"
template <typename T>
auto stack<T>::pop() -> void {
items_.pop_back();
}
template <typename T>
auto stack<T>::push(const T& i) -> void {
items_.push_back(i);
}
auto main() -> int {
stack<int> s;
s.push(5);
}
stack.h
stack.tpp
main.cpp
Static Members
- Each template instantiation has it's own set of static members
#include <vector>
template<typename T>
class stack {
public:
stack();
~stack();
auto push(T&) -> void;
auto pop() -> void;
auto top() -> T&;
auto top() const -> const T&;
static int num_stacks_;
private:
std::vector<T> stack_;
};
template<typename T>
int stack<T>::num_stacks_ = 0;
template<typename T>
stack<T>::stack() {
num_stacks_++;
}
template<typename T>
stack<T>::~stack() {
num_stacks_--;
}
#include <iostream>
#include "./demo706-static.h"
auto main() -> int {
stack<float> fs;
stack<int> is1, is2, is3;
std::cout << stack<float>::num_stacks_ << "\n";
std::cout << stack<int>::num_stacks_ << "\n";
}
demo706-static.h
demo706-static.cpp
Friends
- Each stack instantiation has one unique instantiation of the friend
#include <iostream>
#include <vector>
template<typename T>
class stack {
public:
auto push(T const&) -> void;
auto pop() -> void;
friend auto operator<<(std::ostream& os, stack<T> const& s) -> std::ostream& {
return os << "My top item is " << s.stack_.back() << "\n";
}
private:
std::vector<T> stack_;
};
template<typename T>
auto stack<T>::push(T const& t) -> void {
stack_.push_back(t);
}
#include <iostream>
#include <string>
#include "./stack.h"
auto main() -> int {
stack<std::string> ss;
ss.push("Hello");
std::cout << ss << "\n":
stack<int> is;
is.push(5);
std::cout << is << "\n":
}
demo707-friend.h
demo707-friend.cpp
(Unrelated) Constexpr
- We can provide default arguments to template types (where the defaults themselves are types)
- It means we have to update all of our template parameter lists
Constexpr
- Either:
- A variable that can be calculated at compile time
- A function that, if its inputs are known at compile time, can be run at compile time
#include <iostream>
// This can be called at compile time, or at runtime
constexpr int constexpr_factorial(int n) {
return n <= 1 ? 1 : n * constexpr_factorial(n - 1);
}
// This may not be called at compile time
int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
auto main() -> int {
// Beats a #define any day.
constexpr int max_n = 10;
constexpr int tenfactorial = constexpr_factorial(10);
// This will fail to compile
int ninefactorial = factorial(9);
std::cout << max_n << "\n";
std::cout << tenfactorial << "\n";
std::cout << ninefactorial << "\n";
}
demo708-constexpr.cpp
Constexpr (Benefits)
-
Benefits:
- Values that can be determined at compile time mean less processing is needed at runtime, resulting in an overall faster program execution
- Shifts potential sources of errors to compile time instead of runtime (easier to debug)
COMP6771 20T2 - 7.1 - Templates Intro
By cs6771
COMP6771 20T2 - 7.1 - Templates Intro
- 736