As I learned from a free udemy course1. Code
- Avoiding redefining a function for different data types:
#include<iostream>
using namespace std;
template <typename T>
T Max(T x, T y)
{
return x > y ? x : y;
}
int main()
{
cout<<Max(3,25)<<endl;
cout<<Max(3.24,25.13)<<endl;
}
T is the placeholder for int, double,float… and custom class objects. The type of T is deduced from argument(s)(argument deduction). The catch is, type is deduced from the first argument and following arguments are treated as same type. We can override the deduction for a particular type(template specialisation)
Compiler will generate(template instantiation) separate code for each type. Compiler do this instantiation in the following circumstances:
- A function template is called.
- Taking address of the function template. int (*pfn)(int,int) = Max
- Using explicit instantiation. template char Max(char x, char y)
- Creating explicit specialisation. See an example below:
#include<iostream>
#include <cstring>
using namespace std;
template <typename T>
T Max(T x, T y)
{
return x > y ? x : y;
}
//Optional specification of type
//template <> const char* Max<const char*>
(const char* x,const char* y)
template <> const char* Max(const char* x,const char* y)
{
return strcmp(x,y)>0 ? x : y;
}
int main()
{
cout<<Max(3,25)<<endl;
const char * a {"A"};
const char * b {"B"};
cout<<Max(a,b)<<endl;
}
Non-type template arguments
The usual argument is in a template is T (template <typename T> ). However, we can provide non-type template arguments like int, string, object, etc . Example (and also2) :
#include<iostream>
using namespace std;
// Function to calculate sum of an array
template <typename T, int size>
T Sum(T (&parr)[size])
{
T s{};//initialises to zero
for (int i = 0; i < size; i++) {
s += parr[i];
}
return s;
}
int main ()
{
int a[] = {2,6,7,3};
cout<<"Sum = "<<Sum(a)<<endl;
}
non-type template arguments should be constant expressions and compiler should be able to calculate its value at compile time. The implementation of std::begin and std::end are defined in this way.
Variadic Templates
We can make template accept any number of arguments using initialiser list as follows:
#include<iostream>
using namespace std;
template <typename T>
void print(initializer_list<T> args)
{
for (const auto &x:args) {
cout<<x<<" ";
}
}
int main ()
{
print({1,2,3,4});
return 0;
}
But if we change 2 to 2,.1, it results i an error “deduced conflicting types for parameter ‘_Tp’ (‘int’ and ‘double’)
12 | print({1,2.1,3,4});” This is because it can take only one particular data type. Here we use variadic templates.
#include<iostream>
using namespace std;
//base function for exiting the loop
//when args is 0, uses this function
void Print()
{
cout<<endl;
};
//template parameter pack
template <typename T, typename...Params>
//Function parameter pack
void Print(const T& a, const Params&... args)
{
cout<<a;
if(sizeof...(args)>0){
cout<<", ";
}
Print(args...);//recursive call
}
int main ()
{
Print(1,2.1,3,4);
return 0;
}
Class templates
#include<iostream>
using namespace std;
template <typename T>
class Stack
{
T m_Buffer[512];
int m_Top{-1};
public:
//Default constructor
Stack() = default;
//Copy constructor
Stack(const Stack<T>& s){
m_Top = s.m_Top;
for(int i=0;i<=m_Top;++i){
m_Buffer[i] = s.m_Buffer[i];
}
}
//factory metho for making Stack
static Stack Create();
void Push(const T& elem){
m_Buffer[++m_Top] = elem;
}
const T& Top(){
return m_Buffer[m_Top];
}
void Pop();
bool isEmpty();
};//end of Stack
template<typename T>
void Stack<T>::Pop(){
--m_Top;
}
template<typename T>
bool Stack<T>::isEmpty(){
return m_Top==-1;
}
template <typename T>
Stack<T> Stack<T>::Create(){
return Stack<T>();
}
int main ()
{
Stack<float>a = Stack<float>::Create();
cout<<a.isEmpty()<<endl;
Stack<int> s;
s.Push(-5);s.Push(7);s.Push(9);
s.Push(-1);s.Push(0);
//using copy constructor
auto s2(s);
while( !s2.isEmpty()){
cout<<s2.Top()<<endl;
s2.Pop();
}
return 0;
}
Explicit specialisation for class template
#include<iostream>
using namespace std;
template<typename T>
class prettyPrinter{
T * data_;//pointer
public:
//constructor with initialiser
prettyPrinter(T *data):data_(data){}
//printing the value by dereferencing
void print(){
cout<<"{ "<<*data_<<" }"<<endl;
}
T * getData(){
return data_;//return the pointer
}
};
//explicit specialisation for char
template<>
class prettyPrinter<char*>{
char * data_;//pointer
public:
prettyPrinter(char *data):data_(data){}
void print(){
cout<<"{ "<<data_<<" }"<<endl;
}
char * getData(){
return data_;
}
};
int main()
{
int data_int = 5;
double data_d = 5.001;
prettyPrinter<int> pi(&data_int);
prettyPrinter<double> pd(&data_d);
pi.print();
pd.print();
//error hence call for specialisation
/* char *p {"Hello"};
prettyPrinter<char *> ps(p);
ps.print(); */
char *p {"Hello"};
prettyPrinter<char *> ps(p);
ps.print();
return(0);
}
Specialising only a member function of the class
#include<iostream>
#include<vector>
using namespace std;
template<typename T>
class prettyPrinter{
T * data_;//pointer
public:
//constructor with initialiser
prettyPrinter(T *data):data_(data){}
//printing the value by dereferencing
void print(){
cout<<"{ "<<*data_<<" }"<<endl;
}
T * getData(){
return data_;//return the pointer
}
};
//only print function needs to be modified
//This has to be outside the class
template<>
void prettyPrinter<vector<int>>::print(){
cout<<"{ ";
for (const auto & x: *data_){
cout<<x;
}
cout<<"}";
}
int main()
{
vector<int> v {1,2,3,4,5};
prettyPrinter<vector<int>> pv(&v);
pv.print();
return(0);
}
Partial specialisation
Specifying value for one of the argument.
#include<iostream>
#include<vector>
template<typename T, int columns>
class prettyPrinter{
T * data_;
public:
prettyPrinter(T *data):data_(data){}
void print(){
cout<<"Columns = "<<columns<<endl;
cout<<"{ "<<*data_<<" }"<<endl;
}
T * getData(){
return data_;
}
};
//partial specialisation
template<typename T>
class prettyPrinter<T,80>{
T * data_;
public:
prettyPrinter(T *data):data_(data){}
void print(){
cout<<"Using 80 columns : "<<endl;
cout<<"{ "<<*data_<<" }"<<endl;
}
T * getData(){
return data_;
}
};
int main()
{
int data = 800;
prettyPrinter<int,40> p(&data);
p.print();
//partially specified version is taken
prettyPrinter<int,80> p1(&data);
p1.print();
return(0);
}
typedef(Type Definition)
- Defining a nick name for existing type. Does not introduce a new type.
- Useful to construct a shorter and more meaningful name, hence improve readability.
Examples:
- typedef unsigned integer unit; uint val{}
- typedef long long LLONG; LLONG val{}
- typedef std::vector<std::list<Employee>> Teams; Teams testingTeams
- typedef const char *(*errFn)(int); errFn pfn = getErrorMessage
Type Aliases(same as typedef, introduced in c++11)
- using unit = unsigned integer; uint val{}
- using LLONG = long long; LLONG val{}
- using Teams = std::vector<std::list<Employee>>; Teams testingTeams
- using errFn = const char *(*)(int); errFn pfn = getErrorMessage
#include<iostream>
#include<vector>
#include<list>
using namespace std;
const char *getErrorMessage(int errNo){
return "A message !!";
}
typedef const char *(*PFN1)(int);
using PFN2 = const char *(*)(int);
void showError (PFN1 p){
cout<<p<<endl;
}
typedef vector<list<string>> Names;
using Namez = vector<list<string>>;
template <typename T>
//this is not possible by typedef
using NamezT = vector<list<T>>;
int main()
{
PFN1 p = getErrorMessage;
showError(p);
Names names;
Namez namez;
NamezT<string> sNames;
return(0);
}