# Chapter 16 Templates and Generic Programming

  • 面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况。

    • OOP 能处理类型在程序运行之前都未知的情况;

    • 泛型编程中,在编译时就可以获知类型。

# Defining a Template


# 定义模板

  • 模板:模板是泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。

# 函数模板

  • template <typename T> int compare(const T &v1, const T &v2){}
  • 模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括号 <> 括住的一个或多个模板形参的列表,用逗号分隔,不能为空
  • 使用模板时,我们显式或隐式地指定模板实参,将其绑定到模板参数上。
  • 模板类型参数:类型参数前必须使用关键字 class 或者 typename ,这两个关键字含义相同,可以互换使用。旧的程序只能使用 class
  • 非类型模板参数:表示一个值而非一个类型。实参必须是常量表达式。 template <class T, size_t N> void array_init(T (&parm)[N]){}
  • 内联函数模板: template <typename T> inline T min(const T&, const T&);
  • 模板程序应该尽量减少对实参类型的要求。
  • 函数模板和类模板成员函数的定义通常放在头文件中。

# 类模板

  • 类模板用于生成类的蓝图。
  • 不同于函数模板,编译器不能推断模板参数类型。
  • 定义类模板
    • template <class Type> class Queue {};
  • 实例化类模板:提供显式模板实参列表,来实例化出特定的类。
  • 一个类模板中所有的实例都形成一个独立的类。
  • 模板形参作用域:模板形参的名字可以在声明为模板形参之后直到模板声明或定义的末尾处使用。
  • 类模板的成员函数:
    • template <typename T> ret-type Blob::member-name(parm-list)
  • 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
  • 新标准允许模板将自己的类型参数成为友元。 template <typename T> class Bar{friend T;};
  • 模板类型别名:因为模板不是一个类型,因此无法定义一个 typedef 引用一个模板,但是新标准允许我们为类模板定义一个类型别名: template<typename T> using twin = pair<T, T>;

# 模板参数

  • 模板参数与作用域:一个模板参数名的可用范围是在声明之后,至模板声明或定义结束前。
  • 一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置。
  • 当我们希望通知编译器一个名字表示类型时,必须使用关键字 typename ,而不能使用 class
  • 默认模板实参: template <class T = int> class Numbers{}

# 成员模板

  • 成员模板(member template):本身是模板的函数成员。
    • 普通(非模板)类的成员模板。
    • 类模板的成员模板。

# 控制实例化

  • 动机:在多个文件中实例化相同模板的额外开销可能非常严重。
  • 显式实例化:
    • extern template declaration; // 实例化声明
    • template declaration; // 实例化定义

# 效率与灵活性


# Exercise 16.1

给出实例化的定义。

解:

当编译器实例化一个模版时,它使用实际的模版参数代替对应的模版参数来创建出模版的一个新 “实例”。

# Exercise 16.2

编写并测试你自己版本的 compare 函数。

解:

template<typename T>
int compare(const T& lhs, const T& rhs)
{
	if (lhs < rhs) return -1;
	if (rhs < lhs) return 1;
	return 0;
}

# Exercise 16.3

对两个 Sales_data 对象调用你的 compare 函数,观察编译器在实例化过程中如何处理错误。

解:

error: no match for 'operator<'

# Exercise 16.4

编写行为类似标准库 find 算法的模版。函数需要两个模版类型参数,一个表示函数的迭代器参数,另一个表示值的类型。使用你的函数在一个 vector<int> 和一个 list<string> 中查找给定值。

解:

template<typename Iterator, typename Value>
Iterator find(Iterator first, Iterator last, const Value& v)
{
	for ( ; first != last && *first != value; ++first);
	return first;
}

# Exercise 16.5

为 6.2.4 节中的 print 函数编写模版版本,它接受一个数组的引用,能处理任意大小、任意元素类型的数组。

解:

template<typename Array>
void print(const Array& arr)
{
	for (const auto& elem : arr)
		std::cout << elem << std::endl;
}

# Exercise 16.6

你认为接受一个数组实参的标准库函数 beginend 是如何工作的?定义你自己版本的 beginend

解:

template<typename T, unsigned N>
T* begin(const T (&arr)[N])
{
	return arr;
}
template<typename T, unsigned N>
T* end(const T (&arr)[N])
{
	return arr + N;
}

# Exercise 16.7

编写一个 constexpr 模版,返回给定数组的大小。

解:

template<typename T, typename N> constexpr
unsigned size(const T (&arr)[N])
{
	return N;
}

# Exercise 16.8

在第 97 页的 “关键概念” 中,我们注意到,C++ 程序员喜欢使用 != 而不喜欢 < 。解释这个习惯的原因。

解:

因为大多数类只定义了 != 操作而没有定义 < 操作,使用 != 可以降低对要处理的类型的要求。

# Exercise 16.9

什么是函数模版,什么是类模版?

解:

一个函数模版就是一个公式,可用来生成针对特定类型的函数版本。类模版是用来生成类的蓝图的,与函数模版的不同之处是,编译器不能为类模版推断模版参数类型。如果我们已经多次看到,为了使用类模版,我们必须在模版名后的尖括号中提供额外信息。

# Exercise 16.10

当一个类模版被实例化时,会发生什么?

解:

一个类模版的每个实例都形成一个独立的类。

# Exercise 16.11

下面 List 的定义是错误的。应如何修改它?

template <typename elemType> class ListItem;
template <typename elemType> class List {
public:
	List<elemType>();
	List<elemType>(const List<elemType> &);
	List<elemType>& operator=(const List<elemType> &);
	~List();
	void insert(ListItem *ptr, elemType value);
private:
	ListItem *front, *end;
};

解:

模版需要模版参数,应该修改为如下:

template <typename elemType> class ListItem;  
template <typename elemType> class List{  
public:  
  	List<elemType>();  
  	List<elemType>(const List<elemType> &);  
  	List<elemType>& operator=(const List<elemType> &);  
  	~List();  
  	void insert(ListItem<elemType> *ptr, elemType value);  
private:  
  	ListItem<elemType> *front, *end;  
};

# Exercise 16.12

编写你自己版本的 BlobBlobPtr 模版,包含书中未定义的多个 const 成员。

解:

Blob:

#include <memory>
#include <vector>
template<typename T> class Blob
{
public:
	typedef T value_type;
	typedef typename std::vector<T>::size_type size_type;
	// constructors
	Blob();
	Blob(std::initializer_list<T> il);
	// number of elements in the Blob
	size_type size() const { return data->size(); }
	bool      empty() const { return data->empty(); }
	void push_back(const T& t) { data->push_back(t); }
	void push_back(T&& t) { data->push_back(std::move(t)); }
	void pop_back();
	// element access
	T& back();
	T& operator[](size_type i);
	const T& back() const;
	const T& operator [](size_type i) const;
private:
	std::shared_ptr<std::vector<T>> data;
	// throw msg if data[i] isn't valid
	void check(size_type i, const std::string &msg) const;
};
// constructors
template<typename T>
Blob<T>::Blob() : data(std::make_shared<std::vector<T>>())
{}
template<typename T>
Blob<T>::Blob(std::initializer_list<T> il) :
data(std::make_shared<std::vector<T>>(il))
{}
template<typename T>
void Blob<T>::check(size_type i, const std::string &msg) const
{
	if (i >= data->size())
		throw std::out_of_range(msg);
}
template<typename T>
T& Blob<T>::back()
{
	check(0, "back on empty Blob");
	return data->back();
}
template<typename T>
const T& Blob<T>::back() const
{
	check(0, "back on empty Blob");
	return data->back();
}
template<typename T>
T& Blob<T>::operator [](size_type i)
{
	// if i is too big, check function will throw, preventing access to a nonexistent element
	check(i, "subscript out of range");
	return (*data)[i];
}
template<typename T>
const T& Blob<T>::operator [](size_type i) const
{
	// if i is too big, check function will throw, preventing access to a nonexistent element
	check(i, "subscript out of range");
	return (*data)[i];
}
template<typename T>
void Blob<T>::pop_back()
{
	check(0, "pop_back on empty Blob");
	data->pop_back();
}

BlobPtr:

#include "Blob.h"
#include <memory>
#include <vector>
template <typename> class BlobPtr;
template <typename T>
bool operator ==(const BlobPtr<T>& lhs, const BlobPtr<T>& rhs);
template <typename T>
bool operator < (const BlobPtr<T>& lhs, const BlobPtr<T>& rhs);
template<typename T> class BlobPtr
{
	friend bool operator ==<T>
	(const BlobPtr<T>& lhs, const BlobPtr<T>& rhs);
	friend bool operator < <T>
		(const BlobPtr<T>& lhs, const BlobPtr<T>& rhs);
public:
	BlobPtr() : curr(0) {}
	BlobPtr(Blob<T>& a, std::size_t sz = 0) :
		wptr(a.data), curr(sz)
	{}
	T& operator*() const
	{
		auto p = check(curr, "dereference past end");
		return (*p)[curr];
	}
	// prefix
	BlobPtr& operator++();
	BlobPtr& operator--();
	// postfix
	BlobPtr operator ++(int);
	BlobPtr operator --(int);
private:
	// returns  a shared_ptr to the vector if the check succeeds
	std::shared_ptr<std::vector<T>>
		check(std::size_t, const std::string&) const;
	std::weak_ptr<std::vector<T>> wptr;
	std::size_t curr;
};
// prefix ++
template<typename T>
BlobPtr<T>& BlobPtr<T>::operator ++()
{
	// if curr already points past the end of the container, can't increment it
	check(curr, "increment past end of StrBlob");
	++curr;
	return *this;
}
// prefix --
template<typename T>
BlobPtr<T>& BlobPtr<T>::operator --()
{
	--curr;
	check(curr, "decrement past begin of BlobPtr");
	return *this;
}
// postfix ++
template<typename T>
BlobPtr<T> BlobPtr<T>::operator ++(int)
{
	BlobPtr ret = *this;
	++*this;
	return ret;
}
// postfix --
template<typename T>
BlobPtr<T> BlobPtr<T>::operator --(int)
{
	BlobPtr ret = *this;
	--*this;
	return ret;
}
template<typename T> bool operator==(const BlobPtr<T> &lhs, const BlobPtr<T> &rhs)
{
	if (lhs.wptr.lock() != rhs.wptr.lock())
	{
		throw runtime_error("ptrs to different Blobs!");
	}
	return lhs.i == rhs.i;
}
template<typename T> bool operator< (const BlobPtr<T> &lhs, const BlobPtr<T> &rhs)
{
	if (lhs.wptr.lock() != rhs.wptr.lock())
	{
		throw runtime_error("ptrs to different Blobs!");
	}
	return lhs.i < rhs.i;
}

# Exercise 16.13

解释你为 BlobPtr 的相等和关系运算符选择哪种类型的友好关系?

解:

这里需要与类型一一对应,所以就选择一对一友好关系。

# Exercise 16.14

编写 Screen 类模版,用非类型参数定义 Screen 的高和宽。

解:

Screen

#include <string>
#include <iostream>
template<unsigned H, unsigned W>
class Screen
{
public:
	typedef std::string::size_type pos;
	Screen() = default; // needed because Screen has another constructor
	// cursor initialized to 0 by its in-class initializer
	Screen(char c) :contents(H * W, c) {}
	char get() const              // get the character at the cursor
	{
		return contents[cursor];
	}       // implicitly inline
	Screen &move(pos r, pos c);      // can be made inline later
	friend std::ostream & operator<< (std::ostream &os, const Screen<H, W> & c)
	{
		unsigned int i, j;
		for (i = 0; i<c.height; i++)
		{
			os << c.contents.substr(0, W) << std::endl;
		}
		return os;
	}
	friend std::istream & operator>> (std::istream &is, Screen &  c)
	{
		char a;
		is >> a;
		std::string temp(H*W, a);
		c.contents = temp;
		return is;
	}
private:
	pos cursor = 0;
	pos height = H, width = W;
	std::string contents;
};
template<unsigned H, unsigned W>
inline Screen<H, W>& Screen<H, W>::move(pos r, pos c)
{
	pos row = r * width;
	cursor = row + c;
	return *this;
}

# Exercise 16.15

为你的 Screen 模版实现输入和输出运算符。 Screen 类需要哪些友元(如果需要的话)来令输入和输出运算符正确工作?解释每个友元声明(如果有的话)为什么是必要的。

解:

类的 operator<<operator>> 应该是类的友元。

# Exercise 16.16

StrVec 类重写为模版,命名为 Vec

解:

Vec:

#include <memory>
/**
*  @brief a vector like class
*/
template<typename T>
class Vec
{
public:
	Vec() :element(nullptr), first_free(nullptr), cap(nullptr) {}
	Vec(std::initializer_list<T> l);
	Vec(const Vec& v);
	Vec& operator =(const Vec& rhs);
	~Vec();
	// memmbers
	void push_back(const T& t);
	std::size_t size() const { return first_free - element; }
	std::size_t capacity()const { return cap - element; }
	T* begin() const { return element; }
	T* end()   const { return first_free; }
	void reserve(std::size_t n);
	void resize(std::size_t n);
	void resize(std::size_t n, const T& t);
private:
	// data members
	T* element;
	T* first_free;
	T* cap;
	std::allocator<T> alloc;
	// utillities
	void reallocate();
	void chk_n_alloc() { if (size() == capacity()) reallocate(); }
	void free();
	void wy_alloc_n_move(std::size_t n);
	std::pair<T*, T*> alloc_n_copy(T* b, T* e);
};
// copy constructor
template<typename T>
Vec<T>::Vec(const Vec &v)
{
	/**
	* @brief newData is a pair of pointers pointing to newly allocated and copied
	*        from range : [b, e)
	*/
	std::pair<T*, T*> newData = alloc_n_copy(v.begin(), v.end());
	element = newData.first;
	first_free = cap = newData.second;
}
// constructor that takes initializer_list<T>
template<typename T>
Vec<T>::Vec(std::initializer_list<T> l)
{
	// allocate memory as large as l.size()
	T* const newData = alloc.allocate(l.size());
	// copy elements from l to the address allocated
	T* p = newData;
	for (const auto &t : l)
		alloc.construct(p++, t);
	// build data structure
	element = newData;
	first_free = cap = element + l.size();
}
// operator =
template<typename T>
Vec<T>& Vec<T>::operator =(const Vec& rhs)
{
	// allocate and copy first to protect against self_assignment
	std::pair<T*, T*> newData = alloc_n_copy(rhs.begin(), rhs.end());
	// destroy and deallocate
	free();
	// update data structure
	element = newData.first;
	first_free = cap = newData.second;
	return *this;
}
// destructor
template<typename T>
Vec<T>::~Vec()
{
	free();
}
/**
* @brief   allocate new memeory if nessary and push back the new T
* @param t new T
*/
template<typename T>
void Vec<T>::push_back(const T &t)
{
	chk_n_alloc();
	alloc.construct(first_free++, t);
}
/**
* @brief   preallocate enough memory for specified number of elements
* @param n number of elements required
*/
template<typename T>
void Vec<T>::reserve(std::size_t n)
{
	// if n too small, just return without doing anything
	if (n <= capacity()) return;
	// allocate new memory and move data from old address to the new one
	wy_alloc_n_move(n);
}
/**
*  @brief  Resizes to the specified number of elements.
*  @param  n  Number of elements the %vector should contain.
*
*  This function will resize it to the specified
*  number of elements.  If the number is smaller than the
*  current size it is truncated, otherwise
*  default constructed elements are appended.
*/
template<typename T>
void Vec<T>::resize(std::size_t n)
{
	resize(n, T());
}
/**
*  @brief  Resizes it to the specified number of elements.
*  @param  __new_size  Number of elements it should contain.
*  @param  __x  Data with which new elements should be populated.
*
*  This function will resize it to the specified
*  number of elements.  If the number is smaller than the
*  current size the it is truncated, otherwise
*  the it is extended and new elements are populated with
*  given data.
*/
template<typename T>
void Vec<T>::resize(std::size_t n, const T &t)
{
	if (n < size())
	{
		// destroy the range [element+n, first_free) using destructor
		for (auto p = element + n; p != first_free;)
			alloc.destroy(p++);
		// update first_free to point to the new address
		first_free = element + n;
	}
	else if (n > size())
	{
		for (auto i = size(); i != n; ++i)
			push_back(t);
	}
}
/**
* @brief   allocate new space for the given range and copy them into it
* @param b
* @param e
* @return  a pair of pointers pointing to [first element , one past the last) in the new space
*/
template<typename T>
std::pair<T*, T*>
Vec<T>::alloc_n_copy(T *b, T *e)
{
	// calculate the size needed and allocate space accordingly
	T* data = alloc.allocate(e - b);
	return{ data, std::uninitialized_copy(b, e, data) };
	//            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
	// which copies the range[first, last) to the space to which
	// the starting address data is pointing.
	// This function returns a pointer to one past the last element
}
/**
* @brief   destroy the elements and deallocate the space previously allocated.
*/
template<typename T>
void Vec<T>::free()
{
	// if not nullptr
	if (element)
	{
		// destroy it in reverse order.
		for (auto p = first_free; p != element;)
			alloc.destroy(--p);
		alloc.deallocate(element, capacity());
	}
}
/**
* @brief   allocate memory for spicified number of elements
* @param n
* @note    it's user's responsibility to ensure that @param n is greater than
*          the current capacity.
*/
template<typename T>
void Vec<T>::wy_alloc_n_move(std::size_t n)
{
	// allocate as required.
	std::size_t newCapacity = n;
	T* newData = alloc.allocate(newCapacity);
	// move the data from old place to the new one
	T* dest = newData;
	T* old = element;
	for (std::size_t i = 0; i != size(); ++i)
		alloc.construct(dest++, std::move(*old++));
	free();
	// update data structure
	element = newData;
	first_free = dest;
	cap = element + newCapacity;
}
/**
* @brief   Double the capacity and using std::move move the original data to the newly
*          allocated memory
*/
template<typename T>
void Vec<T>::reallocate()
{
	// calculate the new capacity required
	std::size_t newCapacity = size() ? 2 * size() : 1;
	// allocate and move old data to the new space
	wy_alloc_n_move(newCapacity);
}

# Exercise 16.17

声明为 typename 的类型参数和声明为 class 的类型参数有什么不同(如果有的话)?什么时候必须使用 typename

解:

没有什么不同。当我们希望通知编译器一个名字表示类型时,必须使用关键字 typename ,而不能使用 class

# Exercise 16.18

解释下面每个函数模版声明并指出它们是否非法。更正你发现的每个错误。

(a) template <typename T, U, typename V> void f1(T, U, V);
(b) template <typename T> T f2(int &T);
(c) inline template <typename T> T foo(T, unsigned int *);
(d) template <typename T> f4(T, T);
(e) typedef char Ctype;
	template <typename Ctype> Ctype f5(Ctype a);

解:

  • (a) 非法。应该为 template <typename T, typename U, typename V> void f1(T, U, V);
  • (b) 非法。应该为 template <typename T> T f2(int &t);
  • (c) 非法。应该为 template <typename T> inline T foo(T, unsigned int*);
  • (d) 非法。应该为 template <typename T> T f4(T, T);
  • (e) 非法。 Ctype 被隐藏了。

# Exercise 16.19

编写函数,接受一个容器的引用,打印容器中的元素。使用容器的 size_typesize 成员来控制打印元素的循环。

解:

template<typename Container>
void print(const Container& c)
{
	for (typename Container::size_type i = 0; i != c.size(); ++i)
		std::cout << c[i] << " ";
}

# Exercise 16.20

重写上一题的函数,使用 beginend 返回的迭代器来控制循环。

解:

template<typename Container>
void print(const Container& c)
{
	for (auto it = c.begin(); it != c.end(); ++it)
		std::cout << *it << " ";
}

# Exercise 16.21

编写你自己的 DebugDelete 版本。

解:

DebugDelete

#include <iostream>
class DebugDelete
{
public:
	DebugDelete(std::ostream& s = std::cerr) : os(s) {}
	template<typename T>
	void operator() (T* p) const
	{
		os << "deleting unique_ptr" << std::endl;
		delete p;
	}
private:
	std::ostream& os;
};

# Exercise 16.22

修改 12.3 节中你的 TextQuery 程序,令 shared_ptr 成员使用 DebugDelete 作为它们的删除器。

解:

# Exercise 16.23

预测在你的查询主程序中何时会执行调用运算符。如果你的预测和实际不符,确认你理解了原因。

解:

# Exercise 16.24

为你的 Blob 模版添加一个构造函数,它接受两个迭代器。

解:

template<typename T>    //for class
template<typename It>   //for this member
Blob<T>::Blob(It b, It e) :
    data(std::make_shared<std::vector<T>>(b, e))
{ }

# Exercise 16.25

解释下面这些声明的含义。

extern template class vector<string>;
template class vector<Sales_data>;

解:

前者是模版声明,后者是实例化定义。

# Exercise 16.26

假设 NoDefault 是一个没有默认构造函数的类,我们可以显式实例化 vector<NoDefualt> 吗?如果不可以,解释为什么。

解:

不可以。如

std::vector<NoDefault> vec(10);

会使用 NoDefault 的默认构造函数,而 NoDefault 没有默认构造函数,因此是不可以的。

# Exercise 16.27

对下面每条带标签的语句,解释发生了什么样的实例化(如果有的话)。如果一个模版被实例化,解释为什么;如果未实例化,解释为什么没有。

template <typename T> class Stack {	};
void f1(Stack<char>); 		//(a)
class Exercise {
	Stack<double> &rds;		//(b)
	Stack<int> si;			//(c)
};
int main() {
	Stack<char> *sc;		//(d)
	f1(*sc);				//(e)
	int iObj = sizeof(Stack<string>);	//(f)
}

解:

(a)、(b)、(c)、(f) 都发生了实例化,(d)、(e) 没有实例化。

# Exercise 16.28

编写你自己版本的 shared_ptrunique_ptr

解:

shared_ptr

#pragma once
#include <functional>
#include "delete.h"
namespace cp5
{
	template<typename T>
	class SharedPointer;
	template<typename T>
	auto swap(SharedPointer<T>& lhs, SharedPointer<T>& rhs)
	{
		using std::swap;
		swap(lhs.ptr, rhs.ptr);
		swap(lhs.ref_count, rhs.ref_count);
		swap(lhs.deleter, rhs.deleter);
	}
	template<typename T>
	class SharedPointer
	{
	public:
		//
		//  Default Ctor
		//
		SharedPointer()
			: ptr{ nullptr }, ref_count{ new std::size_t(1) }, deleter{ cp5::Delete{} }
		{}
		//
		//  Ctor that takes raw pointer
		//
		explicit SharedPointer(T* raw_ptr)
			: ptr{ raw_ptr }, ref_count{ new std::size_t(1) }, deleter{ cp5::Delete{} }
		{}
		//
		//  Copy Ctor
		//
		SharedPointer(SharedPointer const& other)
			: ptr{ other.ptr }, ref_count{ other.ref_count }, deleter{ other.deleter }
		{
			++*ref_count;
		}
		//
		//  Move Ctor
		//
		SharedPointer(SharedPointer && other) noexcept
			: ptr{ other.ptr }, ref_count{ other.ref_count }, deleter{ std::move(other.deleter) }
		{
			other.ptr = nullptr;
			other.ref_count = nullptr;
		}
		//
		//  Copy assignment
		//
		SharedPointer& operator=(SharedPointer const& rhs)
		{
			//increment first to ensure safty for self-assignment
			++*rhs.ref_count;
			decrement_and_destroy();
			ptr = rhs.ptr, ref_count = rhs.ref_count, deleter = rhs.deleter;
			return *this;
		}
		//
		//  Move assignment
		//
		SharedPointer& operator=(SharedPointer && rhs) noexcept
		{
			cp5::swap(*this, rhs);
			rhs.decrement_and_destroy();
			return *this;
		}
		//
		//  Conversion operator
		//
		operator bool() const
		{
			return ptr ? true : false;
		}
		//
		//  Dereference
		//
		T& operator* () const
		{
			return *ptr;
		}
		//
		//  Arrow
		//
		T* operator->() const
		{
			return &*ptr;
		}
		//
		//  Use count
		//
		auto use_count() const
		{
			return *ref_count;
		}
		//
		//  Get underlying pointer
		//
		auto get() const
		{
			return ptr;
		}
		//
		//  Check if the unique user
		//
		auto unique() const
		{
			return 1 == *refCount;
		}
		//
		//  Swap
		//
		auto swap(SharedPointer& rhs)
		{
			::swap(*this, rhs);
		}
		//
		// Free the object pointed to, if unique
		//
		auto reset()
		{
			decrement_and_destroy();
		}
		//
		// Reset with the new raw pointer
		//
		auto reset(T* pointer)
		{
			if (ptr != pointer)
			{
				decrement_n_destroy();
				ptr = pointer;
				ref_count = new std::size_t(1);
			}
		}
		//
		//  Reset with raw pointer and deleter
		//
		auto reset(T *pointer, const std::function<void(T*)>& d)
		{
			reset(pointer);
			deleter = d;
		}
		//
		//  Dtor
		//
		~SharedPointer()
		{
			decrement_and_destroy();
		}
	private:
		T* ptr;
		std::size_t* ref_count;
		std::function<void(T*)> deleter;
		auto decrement_and_destroy()
		{
			if (ptr && 0 == --*ref_count)
				delete ref_count,
				deleter(ptr);
			else if (!ptr)
				delete ref_count;
			ref_count = nullptr;
			ptr = nullptr;
		}
	};
}//namespace

unique_ptr:

#include "debugDelete.h"
// forward declarations for friendship
template<typename, typename> class unique_pointer;
template<typename T, typename D> void
swap(unique_pointer<T, D>& lhs, unique_pointer<T, D>& rhs);
/**
*  @brief  std::unique_ptr like class template.
*/
template <typename T, typename D = DebugDelete>
class unique_pointer
{
	friend void swap<T, D>(unique_pointer<T, D>& lhs, unique_pointer<T, D>& rhs);
public:
	// preventing copy and assignment
	unique_pointer(const unique_pointer&) = delete;
	unique_pointer& operator = (const unique_pointer&) = delete;
	// default constructor and one taking T*
	unique_pointer() = default;
	explicit unique_pointer(T* up) : ptr(up) {}
	// move constructor
	unique_pointer(unique_pointer&& up) noexcept
		: ptr(up.ptr) { up.ptr = nullptr; }
	// move assignment
	unique_pointer& operator =(unique_pointer&& rhs) noexcept;
	// std::nullptr_t assignment
	unique_pointer& operator =(std::nullptr_t n) noexcept;
	// operator overloaded :  *  ->  bool
	T& operator  *() const { return *ptr; }
	T* operator ->() const { return &this->operator *(); }
	operator bool() const { return ptr ? true : false; }
	// return the underlying pointer
	T* get() const noexcept{ return ptr; }
	// swap member using swap friend
	void swap(unique_pointer<T, D> &rhs) { ::swap(*this, rhs); }
	// free and make it point to nullptr or to p's pointee.
	void reset()     noexcept{ deleter(ptr); ptr = nullptr; }
	void reset(T* p) noexcept{ deleter(ptr); ptr = p; }
	// return ptr and make ptr point to nullptr.
	T* release();
	~unique_pointer()
	{
		deleter(ptr);
	}
private:
	T* ptr = nullptr;
	D  deleter = D();
};
// swap
template<typename T, typename D>
inline void
swap(unique_pointer<T, D>& lhs, unique_pointer<T, D>& rhs)
{
	using std::swap;
	swap(lhs.ptr, rhs.ptr);
	swap(lhs.deleter, rhs.deleter);
}
// move assignment
template<typename T, typename D>
inline unique_pointer<T, D>&
unique_pointer<T, D>::operator =(unique_pointer&& rhs) noexcept
{
	// prevent self-assignment
	if (this->ptr != rhs.ptr)
	{
		deleter(ptr);
		ptr = nullptr;
		::swap(*this, rhs);
	}
	return *this;
}
// std::nullptr_t assignment
template<typename T, typename D>
inline unique_pointer<T, D>&
unique_pointer<T, D>::operator =(std::nullptr_t n) noexcept
{
	if (n == nullptr)
	{
		deleter(ptr);   ptr = nullptr;
	}
	return *this;
}
// relinquish contrul by returnning ptr and making ptr point to nullptr.
template<typename T, typename D>
inline T*
unique_pointer<T, D>::release()
{
	T* ret = ptr;
	ptr = nullptr;
	return ret;
}

# Exercise 16.29

修改你的 Blob 类,用你自己的 shared_ptr 代替标准库中的版本。

解:

# Exercise 16.30

重新运行你的一些程序,验证你的 shared_ptr 类和修改后的 Blob 类。(注意:实现 weak_ptr 类型超出了本书范围,因此你不能将 BlobPtr 类与你修改后的 Blob 一起使用。)

解:

# Exercise 16.31

如果我们将 DebugDeleteunique_ptr 一起使用,解释编译器将删除器处理为内联形式的可能方式。

解:

# Template Argument Deduction


# 模板实参推断

  • 对函数模板,编译器利用调用中的函数实参来确定其模板参数,这个过程叫模板实参推断

# 类型转换与模板类型参数

  • 能够自动转换类型的只有:
    • 和其他函数一样,顶层 const 会被忽略。
    • 数组实参或函数实参转换为指针。

# 函数模板显式实参

  • 某些情况下,编译器无法推断出模板实参的类型。
  • 定义: template <typename T1, typename T2, typename T3> T1 sum(T2, T3);
  • 使用函数显式实参调用: auto val3 = sum<long long>(i, lng); // T1是显式指定,T2和T3都是从函数实参类型推断而来
  • 注意:正常类型转换可以应用于显式指定的实参。

# 尾置返回类型与类型转换

  • 使用场景:并不清楚返回结果的准确类型,但知道所需类型是和参数相关的。
  • template <typename It> auto fcn(It beg, It end) -> decltype(*beg)
  • 尾置返回允许我们在参数列表之后声明返回类型。

标准库的类型转换模板:

  • 定义在头文件 type_traits 中。
Mod<T> ,其中 Mod 是:T 是:Mod<T>::type 是:
remove_referenceX&X&&X
否则T
add_constX&const X 或函数T
否则const T
add_lvalue_referenceX&T
X&&X&
否则T&
add_rvalue_referenceX&X&&T
否则T&&
remove_pointerX*X
否则T
add_pointerX&X&&X*
否则T*
make_signedunsigned XX
否则T
make_unsigned带符号类型unsigned X
否则T
remove_extentX[n]X
否则T
remove_all_extentsX[n1][n2]...X
否则T

# 函数指针和实参推断

  • 当使用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。

# 模板实参推断和引用

  • 从左值引用函数推断类型:若形如 T& ,则只能传递给它一个左值。但如果是 const T& ,则可以接受一个右值。
  • 从右值引用函数推断类型:若形如 T&& ,则只能传递给它一个右值。
  • 引用折叠和右值引用参数:
    • 规则 1:当我们将一个左值传递给函数的右值引用参数,且右值引用指向模板类型参数时(如 T&& ),编译器会推断模板类型参数为实参的左值引用类型。
    • 规则 2:如果我们间接创造一个引用的引用,则这些引用形成了折叠。折叠引用只能应用在间接创造的引用的引用,如类型别名或模板参数。对于一个给定类型 X
      • X& &X& &&X&& & 都折叠成类型 X&
      • 类型 X&& && 折叠成 X&&
    • 上面两个例外规则导致两个重要结果:
      • 1. 如果一个函数参数是一个指向模板类型参数的右值引用(如 T&& ),则它可以被绑定到一个左值上;
      • 2. 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个左值引用参数( T& )。

# 理解 std::move

  • 标准库 move 函数是使用右值引用的模板的一个很好的例子。
  • 从一个左值 static_cast 到一个右值引用是允许的。
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
  return static_cast<typename remove_reference<T>::type&&>(t);
}

# 转发

  • 使用一个名为 forward 的新标准库设施来传递参数,它能够保持原始实参的类型。
  • 定义在头文件 utility 中。
  • 必须通过显式模板实参来调用。
  • forward 返回显式实参类型的右值引用。即, forward<T> 的返回类型是 T&&

# Exercise 16.32

在模版实参推断过程中发生了什么?

解:

在模版实参推断过程中,编译器使用函数调用中的实参类型来寻找模版实参,用这些模版实参生成的函数版本与给定的函数调用最为匹配。

# Exercise 16.33

指出在模版实参推断过程中允许对函数实参进行的两种类型转换。

解:

  • const 转换:可以将一个非 const 对象的引用(或指针)传递给一个 const 的引用(或指针)形参。
  • 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。

# Exercise 16.34

对下面的代码解释每个调用是否合法。如果合法, T 的类型是什么?如果不合法,为什么?

template <class T> int compare(const T&, const T&);
(a) compare("hi", "world");
(b) compare("bye", "dad");

解:

  • (a) 不合法。 compare(const char [3], const char [6]) , 两个实参类型不一致。
  • (b) 合法。 compare(const char [4], const char [4]) .

# Exercise 16.35

下面调用中哪些是错误的(如果有的话)?如果调用合法, T 的类型是什么?如果调用不合法,问题何在?

template <typename T> T calc(T, int);
tempalte <typename T> T fcn(T, T);
double d; float f; char c;
(a) calc(c, 'c'); 
(b) calc(d, f);
(c) fcn(c, 'c');
(d) fcn(d, f);

解:

  • (a) 合法,类型为 char
  • (b) 合法,类型为 double
  • (c) 合法,类型为 char
  • (d) 不合法,这里无法确定 T 的类型是 float 还是 double

# Exercise 16.36

进行下面的调用会发生什么:

template <typename T> f1(T, T);
template <typename T1, typename T2) f2(T1, T2);
int i = 0, j = 42, *p1 = &i, *p2 = &j;
const int *cp1 = &i, *cp2 = &j;
(a) f1(p1, p2);
(b) f2(p1, p2);
(c) f1(cp1, cp2);
(d) f2(cp1, cp2);
(e) f1(p1, cp1);
(f) f2(p1, cp1);

解:

(a) f1(int*, int*);
(b) f2(int*, int*);
(c) f1(const int*, const int*);
(d) f2(const int*, const int*);
(e) f1(int*, const int*); 这个使用就不合法
(f) f2(int*, const int*);

# Exercise 16.37

标准库 max 函数有两个参数,它返回实参中的较大者。此函数有一个模版类型参数。你能在调用 max 时传递给它一个 int 和一个 double 吗?如果可以,如何做?如果不可以,为什么?

解:

可以。提供显式的模版实参:

int a = 1;
double b = 2;
std::max<double>(a, b);

# Exercise 16.38

当我们调用 make_shared 时,必须提供一个显示模版实参。解释为什么需要显式模版实参以及它是如果使用的。

解:

如果不显示提供模版实参,那么 make_shared 无法推断要分配多大内存空间。

# Exercise 16.39

对 16.1.1 节 中的原始版本的 compare 函数,使用一个显式模版实参,使得可以向函数传递两个字符串字面量。

解:

compare<std::string>("hello", "world")

# Exercise 16.40

下面的函数是否合法?如果不合法,为什么?如果合法,对可以传递的实参类型有什么限制(如果有的话)?返回类型是什么?

template <typename It>
auto fcn3(It beg, It end) -> decltype(*beg + 0)
{
	// 处理序列
	return *beg;
}

解:

合法。该类型需要支持 + 操作。

# Exercise 16.41

编写一个新的 sum 版本,它返回类型保证足够大,足以容纳加法结果。

解:

template<typename T>
auto sum(T lhs, T rhs) -> decltype( lhs + rhs)
{
    return lhs + rhs;
}

# Exercise 16.42

对下面每个调用,确定 Tval 的类型:

template <typename T> void g(T&& val);
int i = 0; const int ci = i;
(a) g(i);
(b) g(ci);
(c) g(i * ci);

解:

(a) int&
(b) const int&
(c) int&&

# Exercise 16.43

使用上一题定义的函数,如果我们调用 g(i = ci) , g 的模版参数将是什么?

解:

i = ci 返回的是左值,因此 g 的模版参数是 int&

# Exercise 16.44

使用与第一题中相同的三个调用,如果 g 的函数参数声明为 T (而不是 T&& ),确定 T 的类型。如果 g 的函数参数是 const T& 呢?

解:

当声明为 T 的时候, T 的类型为 int&
当声明为 const T& 的时候,T 的类型为 int&

# Exercise 16.45

如果下面的模版,如果我们对一个像 42 这样的字面常量调用 g ,解释会发生什么?如果我们对一个 int 类型的变量调用 g 呢?

template <typename T> void g(T&& val) { vector<T> v; }

解:

当使用字面常量, T 将为 int
当使用 int 变量, T 将为 int& 。编译的时候将会报错,因为没有办法对这种类型进行内存分配,无法创建 vector<int&>

# Exercise 16.46

解释下面的循环,它来自 13.5 节中的 StrVec::reallocate :

for (size_t i = 0; i != size(); ++i)
	alloc.construct(dest++, std::move(*elem++));

解:

在每个循环中,对 elem 的解引用操作 * 当中,会返回一个左值, std::move 函数将该左值转换为右值,提供给 construct 函数。

# Exercise 16.47

编写你自己版本的翻转函数,通过调用接受左值和右值引用参数的函数来测试它。

解:

template<typename F, typename T1, typename T2>
void flip(F f, T1&& t1, T2&& t2)
{
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}

# Overloading and Templates


# 重载与模板

  • 多个可行模板:当有多个重载模板对一个调用提供同样好的匹配时,会选择最特例化的版本。
  • 非模板和模板重载:对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。

# Exercise 16.48

编写你自己版本的 debug_rep 函数。

解:

template<typename T> std::string debug_rep(const T& t)
{
    std::ostringstream ret;
    ret << t;
    return ret.str();
}
template<typename T> std::string debug_rep(T* p)
{
    std::ostringstream ret;
    ret << "pointer: " << p;
    if(p)
        ret << " " << debug_rep(*p);
    else
        ret << " null pointer";
    return ret.str();
}

# Exercise 16.49

解释下面每个调用会发生什么:

template <typename T> void f(T);
template <typename T> void f(const T*);
template <typename T> void g(T);
template <typename T> void g(T*);
int i = 42, *p = &i;
const int ci = 0, *p2 = &ci;
g(42); g(p); g(ci); g(p2);
f(42); f(p); f(ci); f(p2);

解:

g(42);    	//g(T )
    g(p);     	//g(T*)
    g(ci);      //g(T)   
    g(p2);      //g(T*)    
    f(42);    	//f(T)
    f(p);     	//f(T)
    f(ci);    	//f(T)
    f(p2);      //f(const T*)

# Exercise 16.50

定义上一个练习中的函数,令它们打印一条身份信息。运行该练习中的代码。如果函数调用的行为与你预期不符,确定你理解了原因。

解:

# Variadic Templates


# 可变参数模板

可变参数模板就是一个接受可变数目参数的模板函数或模板类。

  • 可变数目的参数被称为参数包。
    • 模板参数包:标识另个或多个模板参数。
    • 函数参数包:标识另个或者多个函数参数。
  • 用一个省略号来指出一个模板参数或函数参数,表示一个包。
  • template <typename T, typename... Args>Args 第一个模板参数包。
  • void foo(const T &t, const Args& ... rest);rest 是一个函数参数包。
  • sizeof... 运算符,返回参数的数目。

# 编写可变参数函数模板

  • 可变参数函数通常是递归的:第一步调用处理包中的第一个实参,然后用剩余实参调用自身。

# 包扩展

  • 对于一个参数包,除了获取它的大小,唯一能做的事情就是扩展(expand)。
  • 扩展一个包时,还要提供用于每个扩展元素的模式(pattern)。

# 转发参数包

  • 新标准下可以组合使用可变参数模板和 forward 机制,实现将实参不变地传递给其他函数。

# Exercise 16.51

调用本节中的每个 foo ,确定 sizeof...(Args)sizeof...(rest) 分别返回什么。

解:

#include <iostream>
using namespace std;
template <typename T, typename ... Args>
void foo(const T &t, const Args& ... rest){
    cout << "sizeof...(Args): " << sizeof...(Args) << endl;
    cout << "sizeof...(rest): " << sizeof...(rest) << endl;
};
void test_param_packet(){
    int i = 0;
    double d = 3.14;
    string s = "how now brown cow";
    foo(i, s, 42, d);
    foo(s, 42, "hi");
    foo(d, s);
    foo("hi");
}
int main(){
    test_param_packet();
    return 0;
}

结果:

sizeof...(Args): 3
sizeof...(rest): 3
sizeof...(Args): 2
sizeof...(rest): 2
sizeof...(Args): 1
sizeof...(rest): 1
sizeof...(Args): 0
sizeof...(rest): 0

# Exercise 16.52

编写一个程序验证上一题的答案。

解:

参考 16.51。

# Exercise 16.53

编写你自己版本的 print 函数,并打印一个、两个及五个实参来测试它,要打印的每个实参都应有不同的类型。

解:

template<typename Printable>
std::ostream& print(std::ostream& os, Printable const& printable)
{
    return os << printable;
}
// recursion
template<typename Printable, typename... Args>
std::ostream& print(std::ostream& os, Printable const& printable, Args const&... rest)
{
    return print(os << printable << ", ", rest...);
}

# Exercise 16.54

如果我们对一个没 << 运算符的类型调用 print ,会发生什么?

解:

无法通过编译。

# Exercise 16.55

如果我们的可变参数版本 print 的定义之后声明非可变参数版本,解释可变参数的版本会如何执行。

解:

error: no matching function for call to 'print(std::ostream&)'

# Exercise 16.56

编写并测试可变参数版本的 errorMsg

解:

template<typename... Args>
std::ostream& errorMsg(std::ostream& os, const Args... rest)
{
    return print(os, debug_rep(rest)...);
}

# Exercise 16.57

比较你的可变参数版本的 errorMsg 和 6.2.6 节中的 error_msg 函数。两种方法的优点和缺点各是什么?

解:

可变参数版本有更好的灵活性。

# Exercise 16.58

为你的 StrVec 类及你为 16.1.2 节练习中编写的 Vec 类添加 emplace_back 函数。

解:

template<typename T>        //for the class  template
template<typename... Args>  //for the member template
inline void
Vec<T>::emplace_back(Args&&...args)
{
    chk_n_alloc();
    alloc.construct(first_free++, std::forward<Args>(args)...);
}

# Exercise 16.59

假定 s 是一个 string ,解释调用 svec.emplace_back(s) 会发生什么。

解:

会在 construst 函数中转发扩展包。

# Exercise 16.60

解释 make_shared 是如何工作的。

解:

make_shared 是一个可变模版函数,它将参数包转发然后构造一个对象,再然后一个指向该对象的智能指针。

# Exercise 16.61

定义你自己版本的 make_shared

解:

template <typename T, typename ... Args>
auto make_shared(Args&&... args) -> std::shared_ptr<T>
{
	return std::shared_ptr<T>(new T(std::forward<Args>(args)...));
}

# Template Specializations


# 模板特例化(Specializations)

  • 定义函数模板特例化:关键字 template 后面跟一个空尖括号对( <> )。
  • 特例化的本质是实例化一个模板,而不是重载它。特例化不影响函数匹配。
  • 模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是特例化版本。
  • 我们可以部分特例化类模板,但不能部分特例化函数模板。

# Exercise 16.62

定义你自己版本的 hash<Sales_data> , 并定义一个 Sales_data 对象的 unorder_multise 。将多条交易记录保存到容器中,并打印其内容。

解:

# Exercise 16.63

定义一个函数模版,统计一个给定值在一个 vecor 中出现的次数。测试你的函数,分别传递给它一个 doublevector ,一个 intvector 以及一个 stringvector

解:

#include <iostream>
#include <vector>
#include <cstring>
// template
template<typename T>
std::size_t  count(std::vector<T> const& vec, T value) 
{
    auto count = 0u;
    for(auto const& elem  : vec)
        if(value == elem) ++count;
    return count;
}
// template specialization
template<>
std::size_t count (std::vector<const char*> const& vec, const char* value)
{
    auto count = 0u;
    for(auto const& elem : vec)
        if(0 == strcmp(value, elem)) ++count;
    return count;
}
int main()
{
    // for ex16.63
    std::vector<double> vd = { 1.1, 1.1, 2.3, 4 };
    std::cout << count(vd, 1.1) << std::endl;
    
    // for ex16.64
    std::vector<const char*> vcc = { "alan", "alan", "alan", "alan", "moophy" };
    std::cout << count(vcc, "alan") << std::endl;
    return 0;
}

# Exercise 16.64

为上一题的模版编写特例化版本来处理 vector<const char*> 。编写程序使用这个特例化版本。

解:

参考 16.64。

# Exercise 16.65

在 16.3 节中我们定义了两个重载的 debug_rep 版本,一个接受 const char* 参数,另一个接受 char * 参数。将这两个函数重写为特例化版本。

解:

#include <iostream>
#include <vector>
#include <cstring>
#include <sstream>
// template
template <typename T>
std::string debug_rep(T* t);
// template specialization T=const char*  ,  char*  respectively.
template<>
std::string debug_rep(const char* str);
template<>
std::string debug_rep(      char *str);
int main()
{
    char p[] = "alan";
    std::cout << debug_rep(p) << "\n";
    return 0;
}
template <typename T>
std::string debug_rep(T* t)
{
    std::ostringstream ret;
    ret << t;
    return ret.str();
}
// template specialization
// T = const char*
template<>
std::string debug_rep(const char* str)
{
    std::string ret(str);
    return str;
}
// template specialization
// T =       char*
template<>
std::string debug_rep(      char *str)
{
    std::string ret(str);
    return ret;
}

# Exercise 16.66

重载 debug_rep 函数与特例化它相比,有何优点和缺点?

解:

重载函数会改变函数匹配。

# Exercise 16.67

定义特例化版本会影响 debug_rep 的函数匹配吗?如果不影响,为什么?

解:

不影响,特例化是模板的一个实例,并没有重载函数。

# Chapter Summary

🍓:)