Lesson 04: Generic Programming and Templates
Activity 13: Read Objects from a Connection
- We start by including the headers of the files that provided the connection and the user account object:
#include <iostream>
#include <connection.h>
#include <useraccount.h>
- We can then start to write the writeObjectToConnection function. Declare a template which takes two typename parameters: an Object and a Connection. Call the static method serialize() on the object to get the std::array representing the object, then call writeNext() on the connection to write the data to it:
template<typename Object, typename Connection>
void writeObjectToConnection(Connection& con, const Object& obj) {
std::array<char, 100> data = Object::serialize(obj);
con.writeNext(data);
}
- We can then write readObjectFromConnection. Declare a template taking the same two parameters as before: an Object and a Connection. Inside, we call the connection readNext() to get the data stored inside the connection, then we call the static method on the object type deserialize() to get an instance of the object and return it:
template<typename Object, typename Connection>
Object readObjectFromConnection(Connection& con) {
std::array<char, 100> data = con.readNext();
return Object::deserialize(data);
}
- Finally, in the main function, we can call the functions we created to serialize objects. Both with TcpConnection:
std::cout << “serialize first user account” << std::endl;
UserAccount firstAccount;
TcpConnection tcpConnection;
writeObjectToConnection(tcpConnection, firstAccount);
UserAccount transmittedFirstAccount = readObjectFromConnection<UserAccount>(tcpConnection);
- And with UdpConnection:
std::cout << “serialize second user account” << std::endl;
UserAccount secondAccount;
UdpConnection udpConnection;
writeObjectToConnection(udpConnection, secondAccount);
UserAccount transmittedSecondAccount = readObjectFromConnection<UserAccount>(udpConnection);
The output of the program is as follows:
serialize first user account
the user account has been serialized
the data has been written
the data has been read
the user account has been deserialized
serialize second user account
the user account has been serialized
the data has been written
the data has been read
the user account has been deserialized
Activity 14: UserAccount to Support Multiple Currencies
- We start by including the file defining the currencies:
#include <currency.h>
#include <iostream>
- We then declare the template class Account. It should take a template parameter: Currency. We store the current balance of the account inside a data member of type Currency. We also provide a method in order to extract the current value of the balance:
template<typename Currency>
class Account {
public:
Account(Currency amount) : balance(amount) {}
Currency getBalance() const {
return balance;
}
private:
Currency balance;
};
- Next, we create the method addToBalance. It should be a template with one type parameter, the other currency. The method takes a value of OtherCurrency and converts it to the value of the currency of the current account with the to() function, specifying to which currency the value should be converted to. It then adds it to the balance:
template<typename OtherCurrency>
void addToBalance(OtherCurrency amount) {
balance.d_value += to<Currency>(amount).d_value;
}
- Finally, we can try to call our class in the main function with some data:
Account<GBP> gbpAccount(GBP(1000));
// Add different currencies
std::cout << “Balance: “ << gbpAccount.getBalance().d_value << “ (GBP)” << std::endl;
gbpAccount.addToBalance(EUR(100));
std::cout << “+100 (EUR)” << std::endl;
std::cout << “Balance: “ << gbpAccount.getBalance().d_value << “ (GBP)” << std::endl;
The output of the program is:
Balance: 1000 (GBP)
+100 (EUR)
Balance: 1089 (GBP)
Activity 15: Write a Matrix Class for Mathematical Operations in a Game
- We start by defining a Matrix class which takes three template parameters: one type and the two dimensions of the Matrix class. The dimensions are of type int. Internally, we create a std::array with the size of the number of rows times the number of columns, in order to have enough space for all elements of the matrix. We add a constructor to initialize the array to empty, and a constructor to provide a list of values:
#include <array>
template<typename T, int R, int C>
class Matrix {
// We store row_1, row_2, ..., row_C
std::array<T, R*C> data;
public:
Matrix() : data({}) {}
Matrix(std::array<T, R*C> initialValues) : data(initialValues) {}
};
- We add a method get() to the class to return a reference to the element T. The method needs to take the row and column we want to access.
- We make sure that the requested indexes are inside the bounds of the matrix, otherwise we call std::abort(). In the array, we first store all the elements of the first row, then all the elements of the second row, and so on. When we want to access the elements of the nth row, we need to skip all the elements of the previous rows, which are going to be the number of elements per row (so the number of columns) times the previous rows, resulting in the following method:
T& get(int row, int col) {
if (row >= R || col >= C) {
std::abort();
}
return data[row*C + col];
}
- For convenience, we define a function to print the class as well. We print all the elements in the columns separated by spaces, with one column per line:
template<typename T, size_t R, size_t C>
std::ostream& operator<<(std::ostream& os, Matrix<T, R, C> matrix) {
os << ‘\n’;
for(int r=0; r < R; r++) {
for(int c=0; c < C; c++) {
os << matrix.get(r, c) << ‘ ‘;
}
os << “\n”;
}
return os;
}
- In the main function, we can now use the functions we have defined:
Matrix<int, 3, 2> matrix({
1, 2,
3, 4,
5, 6
});
std::cout << “Initial matrix:” << matrix << std::endl;
matrix.get(1, 1) = 7;
std::cout << “Modified matrix:” << matrix << std::endl;
The output is as follows:
Initial matrix:
1 2
3 4
5 6
Modified matrix:
1 2
3 7
5 6
Solution bonus step:
- We can add a new method, multiply, which takes a std::array of type T with the length of C by const reference, since we are not modifying it.
The function returns an array of the same type, but length R.
- We follow the definition of matrix-vector multiplication to compute the result:
std::array<T, R> multiply(const std::array<T, C>& vector){
std::array<T, R> result = {};
for(size_t r = 0; r < R; r++) {
for(size_t c = 0; c < C; c++) {
result[r] += get(r, c) * vector[c];
}
}
return result;
}
- We can now extend our main function to call the multiply function:
std::array<int, 2> vector = {8, 9};
std::array<int, 3> result = matrix.multiply(vector);
std::cout << “Result of multiplication: [“ << result[0] << “, “
<< result[1] << “, “ << result[2] << “]” << std::endl;
The output is as follows:
Result of multiplication: [26, 87, 94]
Activity 16: Make the Matrix Class Easier to Use
- We start by importing <functional> in order to have access to std::multiplies:
#include <functional>
- We then change the order of the template parameters in the class template, so that the size parameters come first. We also add a new template parameter, Multiply, which is the type we will use for computing the multiplication between the elements in the vector by default, and we store an instance of it in the class:
template<int R, int C, typename T = int, typename Multiply=std::multiplies<T> >
class Matrix {
std::array<T, R*C> data;
Multiply multiplier;
public:
Matrix() : data({}), multiplier() {}
Matrix(std::array<T, R*C> initialValues) : data(initialValues), multiplier() {}
};
The get() function remains the same as the previous activity.
- We now need to make sure that the Multiply method uses the Multiply type provided by the user to perform the multiplication.
- To do so, we need to make sure to call multiplier(operand1, operand2) instead of operand1 * operand2, so that we use the instance we stored inside the class:
std::array<T, R> multiply(const std::array<T, C>& vector) {
std::array<T, R> result = {};
for(int r = 0; r < R; r++) {
for(int c = 0; c < C; c++) {
result[r] += multiplier(get(r, c), vector[c]);
}
}
return result;
}
- We can now add an example of how we can use the class:
// Create a matrix of int, with the ‘plus’ operation by default
Matrix<3, 2, int, std::plus<int>> matrixAdd({
1, 2,
3, 4,
5, 6
});
std::array<int, 2> vector = {8, 9};
// This will call std::plus when doing the multiplication
std::array<int, 3> result = matrixAdd.multiply(vector);
std::cout << “Result of multiplication(with +): [“ << result[0] << “, “
<< result[1] << “, “ << result[2] << “]” << std::endl;
The output is as follows:
Result of multiplication(with +): [20, 24, 28]
Activity 17: Ensure Users are Logged in When Performing Actions on the Account
- We first declare a template function which takes two type parameters: an Action and a Parameter type.
- The function should take the user identification, the action and the parameter. The parameter should be accepted as a forwarding reference. As a first step, it should check if the user is logged in, by calling the isLoggenIn() function. If the user is logged in, it should call the getUserCart() function, then call the action passing the cart and forwarding the parameter:
template<typename Action, typename Parameter>
void execute_on_user_cart(UserIdentifier user, Action action, Parameter&& parameter) {
if(isLoggedIn(user)) {
Cart cart = getUserCart(user);
action(cart, std::forward<Parameter>(parameter));
} else {
std::cout << “The user is not logged in” << std::endl;
}
}
- We can test how execute_on_user_cart works by calling it in the main function:
Item toothbrush{1023};
Item toothpaste{1024};
UserIdentifier loggedInUser{0};
std::cout << “Adding items if the user is logged in” << std::endl;
execute_on_user_cart(loggedInUser, addItems, std::vector<Item>({toothbrush, toothpaste}));
UserIdentifier loggedOutUser{1};
std::cout << “Removing item if the user is logged in” << std::endl;
execute_on_user_cart(loggedOutUser, removeItem, toothbrush);
The output is as follows:
Adding items if the user is logged in
Items added
Removing item if the user is logged in
The user is not logged in
Activity 18: Safely Perform Operations on User Cart with an Arbitrary Number of Parameters
- We need to expand the previous activity to accept any number of parameters with any kind of ref-ness and pass it to the action provided. To do so, we need to create a variadic template.
- Declare a template function that takes an action and a variadic number of parameters as template parameters. The function parameters should be the user action, the action to perform, and the expanded template parameter pack, making sure that the parameters are accepted as forwarding references.
- Inside the function, we perform the same checks as before, but now we expand the parameters when we forward them to the action:
template<typename Action, typename... Parameters>
void execute_on_user_cart(UserIdentifier user, Action action, Parameters&&... parameters) {
if(isLoggedIn(user)) {
Cart cart = getUserCart(user);
action(cart, std::forward<Parameters>(parameters)...);
} else {
std::cout << “The user is not logged in” << std::endl;
}
}
- Let’s test the new function in our main function:
Item toothbrush{1023};
Item apples{1024};
UserIdentifier loggedInUser{0};
std::cout << “Replace items if the user is logged in” << std::endl;
execute_on_user_cart(loggedInUser, replaceItem, toothbrush, apples);
UserIdentifier loggedOutUser{1};
std::cout << “Replace item if the user is logged in” << std::endl;
execute_on_user_cart(loggedOutUser, removeItem, toothbrush);
The output is as follows:
Replace items if the user is logged in
Replacing item
Item removed
Items added
Replace item if the user is logged in
The user is not logged in