COMP6771

Advanced C++ Programming

Week 8.1

Advanced Templates

Default Members

  • 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
#include <vector>

template<typename T, typename CONT = std::vector<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:
	CONT stack_;
};

template<typename T, typename CONT>
int stack<T, CONT>::num_stacks_ = 0;

template<typename T, typename CONT>
stack<T, CONT>::stack() {
	num_stacks_++;
}

template<typename T, typename CONT>
stack<T, CONT>::~stack() {
	num_stacks_--;
}
#include <iostream>

#include "./demo710-default.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";
}

demo801-default.h

demo801-default.cpp

Specialisation

  • The templates we've defined so far are completely generic
  • There are two ways we can redefine our generic types for something more specific:

When to specialise

  • You need to preserve existing semantics for something that would not otherwise work
    • std::is_pointer is partially specialised over pointers
  • You want to write a type trait
    • std::is_integral is fully specialised for int, long, etc.
  • There is an optimisation you can make for a specific type
    • std::vector<bool> is fully specialised to reduce memory footprint

When not to specialise

  • Don't specialise functions
  • You think it would be cool if you changed some feature of the class for a specific type
    • People assume a class works the same for all types
    • Don't violate assumptions!

Our Template

  • Here is our stack template class
    • stack.h
    • stack_main.cpp
#include <vector>
#include <iostream>
#include <numeric>

template <typename T>
class stack {
public:
	auto push(T t) -> void { stack_.push_back(t); }
	auto top() -> T& { return stack_.back(); }
	auto pop() -> void { stack_.pop_back(); }
	auto size() const -> int { return stack_.size(); };
	auto sum() -> int {
		return std::accumulate(stack_.begin(), stack_.end(), 0);
	}
private:
	std::vector<T> stack_;
};
auto main() -> int {
	int i1 = 6771;
	int i2 = 1917;

	stack<int> s1;
	s1.push(i1);
	s1.push(i2);
	std::cout << s1.size() << " ";
	std::cout << s1.top() << " ";
	std::cout << s1.sum() << "\n";
}

Partial Specialisation

  • In this case we will specialise for pointer types.
    • Why do we need to do this?
  • You can partially specialise classes
    • You cannot partially specialise a particular function of a class in isolation
  • The following a fairly standard example, for illustration purposes only. Specialisation is designed to refine a generic implementation for a specific type, not to change the semantic.
template <typename T>
class stack<T*> {
public:
	auto push(T* t) -> void { stack_.push_back(t); }
	auto top() -> T* { return stack_.back(); }
	auto pop() -> void { stack_.pop_back(); }
	auto size() const -> int { return stack_.size(); };
	auto sum() -> int{
		return std::accumulate(stack_.begin(),
          stack_.end(), 0, [] (int a, T *b) { return a + *b; });
	}
private:
	std::vector<T*> stack_;
};
auto main() -> int {
	int i1 = 6771;
	int i2 = 1917;
	stack<int*> s2;
	s2.push(&i1);
	s2.push(&i2);
	std::cout << s2.size() << " ";
	std::cout << *(s2.top()) << " ";
	std::cout << s2.sum() << "\n";
}

demo802-partial.h

demo802-partial.cpp

Explicit Specialisation

#include <iostream>

template <typename T>
struct is_void {
	static bool const val = false;
};

template<>
struct is_void<void> {
	static bool const val = true;
};

auto main() -> int {
	std::cout << is_void<int>::val << "\n";
	std::cout << is_void<void>::val << "\n";
}

demo803-explicit.cpp

Type Traits

  • Trait: Class (or clas template) that characterises a type
#include <iostream>
#include <limits>

auto main() -> int {
	std::cout << std::numeric_limits<double>::min() << "\n";
	std::cout << std::numeric_limits<int>::min() << "\n";
}
template <typename T>
struct numeric_limits {
	static auto min() -> T;
};

template <>
struct numeric_limits<int> {
	static auto min() -> int { return -INT_MAX - 1; }
}

template <>
struct numeric_limits<float> {
	static auto min() -> int { return -FLT_MAX - 1; }
}

This is what <limits> might look like

Type Traits

  • Traits allow generic template functions to be parameterised
#include <array>
#include <iostream>
#include <limits>

template <typename T, std::size_t size>
T findMax(const std::array<T, size>& arr) {
	T largest = std::numeric_limits<T>::min();
	for (auto const& i : arr) {
		if (i > largest) largest = i;
	}
	return largest;
}

auto main() -> int {
	std::array<int, 3> i{ -1, -2, -3 };
	std::cout << findMax<int, 3>(i) << "\n";
	std::array<double, 3> j{ 1.0, 1.1, 1.2 };
	std::cout << findMax<double, 3>(j) << "\n";
}

demo804-typetraits1.cpp

Two more examples

  • Below are STL type trait examples for a specialisation and partial specialisation
  • This is a good example of partial specialisation
  • http://en.cppreference.com/w/cpp/header/type_traits
#include <iostream>

template <typename T>
struct is_void {
	static const bool val = false;
};

template<>
struct is_void<void> {
	static const bool val = true;
};

auto main() -> int {
	std::cout << is_void<int>::val << "\n";
	std::cout << is_void<void>::val << "\n";
}
#include <iostream>

template <typename T>
struct is_pointer {
	static const bool val = false;
};

template<typename T>
struct is_pointer<T*> {
	static const bool val = true;
};

auto main() -> int {
	std::cout << is_pointer<int*>::val << "\n";
	std::cout << is_pointer<int>::val << "\n";
}

demo805-typetraits2.cpp

demo806-typetraits3.cpp

Where it's useful

  • Below are STL type trait examples
  • http://en.cppreference.com/w/cpp/header/type_traits
#include <iostream>
#include <type_traits>

template <typename T>
auto testIfNumberType(T i) -> void {
	if (std::is_integral<T>::value || std::is_floating_point<T>::value) {
		std::cout << i << " is a number" << "\n";
	} else {
		std::cout << i << " is not a number" << "\n";
	}
}

auto main() -> int {
	int i = 6;
	long l = 7;
	double d = 3.14;
	testIfNumberType(i);
	testIfNumberType(l);
	testIfNumberType(d);
	testIfNumberType(123);
	testIfNumberType("Hello");
	auto s = "World";
	testIfNumberType(s);
}

demo807-typetraits4.cpp

Variadic Templates

  • These are the instantiations that will have been generated
#include <iostream>
#include <typeinfo>

template <typename T>
auto print(const T& msg) -> void {
	std::cout << msg << " ";
}

template <typename A, typename... B>
auto print(A head, B... tail) -> void {
	print(head);
	print(tail...);
}

auto main() -> int {
	print(1, 2.0f);
	std::cout << "\n";
	print(1, 2.0f, "Hello");
	std::cout << "\n";
}
auto print(const char* const& c) -> void {
	std::cout << c << " ";
}

auto print(float const& b) -> void {
	std::cout << b << " ";
}

auto print(float b, const char* c) -> void {
	print(b);
	print(c);
}

auto print(int const& a) -> void {
	std::cout << a << " ";
}

auto print(int a, float b, const char* c) -> void {
	print(a);
	print(b, c);
}

demo808-variadic.cpp

Member Templates

  • Sometimes templates can be too rigid for our liking:
    • Clearly, this could work, but doesn't by default
#include <vector>

template <typename T>
class stack {
public:
	auto push(T& t) -> void { stack._push_back(t); }
	auto top() -> T& { return stack_.back(); }
private:
	std::vector<T> stack_;
};

auto main() -> int {
	stack<int> is1;
	is1.push(2);
	is1.push(3);
	stack<int> is2{is1}; // this works
	stack<double> ds1{is1}; // this does not
}

Member Templates

  • Through use of member templates, we can extend capabilities
#include <vector>

template <typename T>
class stack {
public:
	explicit stack() {}
	template <typename T2>
	stack(stack<T2>&);
	auto push(T t) -> void { stack_.push_back(t); }
	auto pop() -> T;
	auto empty() const -> bool { return stack_.empty(); }
private:
	std::vector<T> stack_;
};

template <typename T>
T stack<T>::pop() {
	T t = stack_.back();
	stack_.pop_back();
    return t;
}

template <typename T>
template <typename T2>
stack<T>::stack(stack<T2>& s) {
	while (!s.empty()) {
		stack_.push_back(static_cast<T>(s.pop()));
	}
}
auto main() -> int {
	stack<int> is1;
	is1.push(2);
	is1.push(3);
	stack<int> is2{is1}; // this works
	stack<double> ds1{is1}; // this does not
}

demo809-membertemp.cpp

Template Template Parameters

  • Previously, when we want to have a Stack with templated container type we had to do the following:
    • What is the issue with this?
template <typename T, template <typename> typename CONT>
class stack {}
#include <iostream>
#include <vector>

auto main(void) -> int {
	stack<int, std::vector<int>> s1;
	s1.push(1);
	s1.push(2);
	std::cout << "s1: " << s1 << "\n";

	stack<float, std::vector<float>> s2;
	s2.push(1.1);
	s2.push(2.2);
	std::cout << "s2: " << s2 << "\n";
	//stack<float, std::vector<int>> s2; :O
} 

Ideally we can just do:

#include <iostream>
#include <vector>

auto main(void) -> int {
  stack<int, std::vector> s1;
  s1.push(1);
  s1.push(2);
  std::cout << "s1: " << s1 << std::endl;

  stack<float, std::vector> s2;
  s2.push(1.1);
  s2.push(2.2);
  std::cout << "s2: " << s2 << std::endl;
} 

Template Template Parameters

#include <iostream>
#include <vector>

template <typename T, typename Cont>
class stack {
public:
	auto push(T t) -> void { stack_.push_back(t); }
	auto pop() -> void { stack_.pop_back(); }
	auto top() -> T& { return stack_.back(); }
	auto empty() const -> bool { return stack_.empty(); }
private:
	CONT stack_;
};
#include <iostream>
#include <vector>
#include <memory>

template <typename T, template <typename...> typename CONT>
class stack {
public:
	auto push(T t) -> void { stack_.push_back(t); }
	auto pop() -> void { stack_.pop_back(); }
	auto top() -> T& { return stack_.back(); }
	auto empty() const -> bool { return stack_.empty(); }
private:
	CONT<T> stack_;
};
#include <iostream>
#include <vector>

auto main(void) -> int {
	stack<int, std::vector> s1;
	s1.push(1);
	s1.push(2);
} 
auto main(void) -> int {
	stack<int, std::vector<int>> s1;
	int i1 = 1;
	int i2 = 2;
	s1.push(i1);
	s1.push(i2);
	while (!s1.empty()) {
		std::cout << s1.top() << " ";
		s1.pop();
	}
	std::cout << "\n";
} 

demo810-temptemp.cpp

Template Argument Deduction

Template Argument Deduction is the process of determining the types (of type parameters) and the values of nontype parameters from the types of function arguments.

template <typename T, int size>
T findmin(const T (&a)[size]) {
  T min = a[0];
  for (int i = 1; i < size; i++) {
    if (a[i] < min) min = a[i];
  }
  return min;
}

type paremeter

non-type parameter

call parameters

Implicit Deduction

  • Non-type parameters: Implicit conversions behave just like normal type conversions
  • Type parameters: Three possible implicit conversions
  • ... others as well, that we won't go into
// array to pointer
template <typename T>
f(T* array) {}

int a[] = { 1, 2 };
f(a);
// const qualification
template <typename T>
f(const T item) {}

int a = 5;
f(a); // int => const int;
// conversion to base class
//  from derived class
template <typename T>
void f(base<T> &a) {}

template <typename T>
class derived : public base<T> { }
derived<int> d;
f(d);

Explicit Deduction

  • If we need more control over the normal deduction process, we can explicitly specify the types being passed in
template <typename T>
T min(T a, T b) {
  return a < b ? a : b;
}

auto main() -> int {
  int i; double d;
  min(i, static_cast<int>(d)); // int min(int, int)
  min<int>(i, d); // int min(int, int)
  min(static_cast<double>(i), d); // double min(double, double)
  min<double>(i, d); // double min(double, double)
}

demo811-explicitdeduc.cpp

COMP6771 20T2 - 8.1 - Advanced Templates

By cs6771

COMP6771 20T2 - 8.1 - Advanced Templates

  • 475
Loading comments...

More from cs6771