C++ 小白 学习记录16

2021/9/30 17:11:04

本文主要是介绍C++ 小白 学习记录16,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

第十六章 模版与泛型编程

 

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

泛型编程: 在编译时就能获知类型

泛型代码的两个重要原则:

  • 模板中的函数参数是const的应用(使用const可以接受更多的类型, 拷贝/不可拷贝/const/非const的)
  • 函数体重的条件判断仅使用<比较运算符(减少对类型的要求, <可以计算出!=, >, ==).

16.1 定义模版

16.1.1 函数模版

代表 任意类型.  类型参数

template <typename T, typename W, typename A >  // 指定一个类型为T
int compare(const T& v1, const W& v2) { // 模版中指定的类型
	if (v1 < v2) return -1;
	if (v2 < v1) return 1;
	return 0;
}

模版参数列表的作用很像函数参数列表

类型参数前必须使用class 或 typename, 在模板参数列表中, 这俩没有什么不同.

类型已经指定了, 代表任意值. 非类型参数, 要求必须是常量表达式.

template<unsigned N, unsigned M>
int compare(const char(&p1)[N], const char(&p2)[m]) {
	return strcmp(p1, p2);
}

inline和constexpr 的函数模板:

inline或constexpr放在模板参数之后, 返回类型之前

template <typename T> inline T min(const T&, const T&);

函数模板和类模板成员函数的定义通常放在头文件中.

16.1.2 类模板

编译器不能为类模板推断模板参数类型. 感觉就像 需要使用string的地方全部换成T.

template<typename T> 
class Blob {
public:
	typedef T value_type;
	typedef typename std::vector<T>::size_type size_type;

	Blob():data(std::make_shared<std::vector<T>>()) {};
	Blob(std::initializer_list<T> il) :data(std::make_shared<std::vector<T>>(il)) {};
	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();
	T& back();
	T& operator[](size_type i);
private:
	std::shared_ptr<std::vector<T>> data;
	Blob check(size_type i, const T& msg) const;
};

模板形成的各个类 之间没有任何关系.

当在类外完成内类声明的函数时, 仍然需要使用template<typename T> 声明类型T, 并且 需要用到类名的地方, 必须在类名后面增加<T>, 如 check函数的定义为:

template<typename T>
Blob<T> Blob<T>::check(size_type i, const T& msg) const { // 类模板外使用 类模板名时需要带<T>
	// xx
}

使用类模板时:

int main(int argc, char** argv) {
	Blob<int> ia = { 0,1,2,3,4 };
}

模板嵌套, 和在类模板作用域内时可以省略<T>, 在类模板外使用类模板名时需要BlobPtr<T>

template <typename T> class BlobPtr {
public:
	BlobPtr() :curr(0) {}
	BlobPtr(Blob<T> &a, size_t sz=0):wptr(a.data), curr(sz) {} // 使用别的类模板时, 需要Blob<T>
	T& operator*() const {
		auto p = check(curr, "dereference past end");
		return (*p)[curr];
	}
	BlobPtr& operator++(); // 类模板作用域内 不需要使用BlobPtr<T>& operator++()
	BlobPtr& operator--(); // 等同于 BlobPtr<T>& operator--()
private:
	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;
};

类模板和友元:

类模板包含的非模板友元, 则友元可以访问所有模板实例

类模板包含模板友元, 则所有友元实例可以访问所有模板实例, 也可以只授权特定友元.

一对一友好关系:

template <typename> class BlobPtr; // Blob内定义友元类 使用

template<typename> class Blob; // 用于operator==的函数声明
template<typename T> // 用于operator==的函数声明
bool operator==(const Blob<T>&, const Blob<T>&); // 函数声明

template<typename T> 
class Blob {
public:
	// typedef T value_type;
	typedef typename std::vector<T>::size_type size_type;

	friend class BlobPtr<T>; //友元类
	friend bool operator==<T>(const Blob<T>&, const Blob<T>&); // 友元函数声明
    // xx
}

上述代码中, 友元类的声明中 BlobPtr<T> 和友元函数 在Blob中的声明中, 使用的类型都是T, 如此则限定了, BlobPtr, operator= 中使用的类型必须和Blob中的类型一致. 即: 友好关系 被限定在所有使用相同类型的Blob, BlobPtr, operator== 中.

通用和特定的模板友好关系:

分为两种, 一种是非模板类 对模板类:

template<typename T> class Pal; //类模板的前置声明
class C {
	friend class Pal<C>; // 只用用C实例化的类Pal才是C的一个友元.
	template <typename T> friend class Pal2; // Pal2的所有实例都是C的友元, 此种情况不需要前置声明
};

 另一种, 模板类对 其他:

template<typename T> class Pal; // 类模板的前置声明
template<typename T> class C2 {
	friend class Pal<T>; // 类型为T的C2的实例和Pal的实例才是友好关系. 一对一
	template <typename X> friend class Pal2; // X可能等于T,也可能不等于T, 所有就是任意类型的C2的实例和Pal2实例都是友元关系, 多对多
	friend class Pal3; // Pal3 不需要前置声明, 所有类型的C2实例都与Pal3 是友元关系, 多对一.

};

上面代码中, 两种情况下都是一对一的需要前置声明, 其余的则不需要.

模板参数也可以声明为友元:

template<typename T> class B {
	friend T;
};

模板类型别名:  因为Blob<T>不是一个确切的类型, 所有不能使用typedef 重命名, 必须是确定的类型才可以重命名; 但是 可以这么命名

typedef Blob<T> StrBlob;  // 错误
typedef Blob<string> StrBlob;

template<typename T> using twin = pair<T, T>; // 定义
twin<string>authors; // == pair<string, string>  使用方法

template<typename T> using twin2 = pair<int, T>; // 也可以固定一个类型

类模板的static成员: 所有的类模板实例 都共享相同的static成员

template<typename T> class Foo {
public:
	static std::size_t cout() { return ctr; }
private:
	static std::size_t ctr;
};
template<typename T> size_t Foo<T>::ctr = 0; // 定义 跟非static成员定义相似

int main(int argc, char** argv) {
	Foo<int> fi;
	auto ct = Foo<int>::cout();
	ct = fi.cout();
	ct = Foo::cout(); // 错误, 无法确定模板实例, 所以无法使用cout函数
}

static 成员需要在模板外定义初始值

模板外部使用模板名时需要携带类型列表

// 16.16
template <typename T>
class Vec {
public:
	Vec() :elements(nullptr), first_free(nullptr), cap(nullptr) {}
	Vec(const Vec&);
	Vec(const std::initializer_list<T>&);
	Vec& operator=(const Vec&);
	Vec& operator=(Vec&&) noexcept;
	Vec(Vec&&) noexcept;
	~Vec();

	void push_back(const T&);
	void push_back(const T&&);
	size_t size() const { return first_free - elements; }
	size_t capacity() const { return cap - elements; }

	void resize(size_t);
	void resize(size_t, const T&);

	// 若 new_cap 大于当前的 capacity() ,则分配新存储,否则该方法不做任何事。
	void reserve(size_t);  // 增加vector的容量到大于或者等于 指定的值
	T* begin() const { return elements; }
	T* end() const { return first_free; }

private:
	static std::allocator<T> alloc; // 用于分配元素内存
	// 如果没有空间容纳新元素了, 则重新分配内存
	void chk_n_alloc() {
		if (size() == capacity()) reallocate();
	}
	// 用于分配足够的内存保存给定范围的元素, 并将这些元素拷贝到新分配的内存中.返回的pair中的两个指针
	// 分别指向新空间的开始位置和尾后位置.
	std::pair<T*, T*>alloc_n_copy(const T*, const T*);
	void free(); // 销毁元素并释放内存
	void reallocate(size_t); // 获得更多内存并拷贝已有元素
	T* elements; // 指向首元素的指针
	T* first_free; // 指向第一个空闲元素的指针
	T* cap; // 指向尾后位置的指针
};
template<typename T>
std::allocator<T> Vec<T>::alloc = std::allocator<T>();

template<typename T>
void Vec<T>::push_back(const T& s) {
	chk_n_alloc();
	alloc.construct(first_free++, s);
}
template<typename T>
void Vec<T>::push_back(const T&& s) {
	chk_n_alloc();
	alloc.construct(first_free++, std::move(s));
}
template<typename T>
std::pair<T*, T*>Vec<T>::alloc_n_copy(const T* b, const T* e) {
	auto data = alloc.allocate(e - b);
	return { data, uninitialized_copy(b,e, data) };
}
template<typename T>
void Vec<T>::free() {
	// elements 如果为空 说明该vec为空, 则不必释放内存
	if (elements) {
		for_each(elements, first_free, [this](T& s) { alloc.destroy(&s); });
		alloc.deallocate(elements, cap - elements);
	}
}
template<typename T>
Vec<T>::Vec(const Vec<T>& s) {
	auto newData = alloc_n_copy(s.begin(), s.end());
	elements = newData.first;
	first_free = cap = newData.second;
}
template<typename T>
Vec<T>::~Vec() {
	free();
}
template<typename T>
Vec<T>& Vec<T>::operator=(const Vec<T>& rhs) {
	auto data = alloc_n_copy(rhs.begin(), rhs.end());
	free();
	elements = data.first;
	first_free = cap = data.second;
	return *this;
}
template<typename T>
Vec<T>& Vec<T>::operator=(Vec<T>&& rrs) noexcept {
	if (this != &rrs) {
		free();
		elements = rrs.elements;
		first_free = rrs.first_free;
		cap = rrs.cap;
		rrs.elements = rrs.first_free = rrs.cap = nullptr;
	}
	return *this;
};
template<typename T>
void Vec<T>::reallocate(size_t new_size) {
	size_t newcapacity = 0;
	if (new_size == 0) newcapacity = size() ? 2 * size() : 1;
	else newcapacity = new_size;
	auto newdata = alloc.allocate(newcapacity);
	auto dest = newdata;
	auto elem = elements;
	for (size_t i = 0; i != size(); ++i)
		alloc.construct(dest++, std::move(*elem++));
	free();
	elements = newdata;
	first_free = dest;
	cap = elements + newcapacity;
}
template<typename T>
void Vec<T>::reserve(size_t new_cap) {
	if (new_cap > capacity()) {
		reallocate(new_cap);
	}
}
template<typename T>
void Vec<T>::resize(size_t count) { resize(count, T()); }
template<typename T>
void Vec<T>::resize(size_t count, const T& s) {
	if (count > size()) {
		if (count > capacity()) reallocate(2 * count);
		for (size_t t = size(); t != count; ++t) {
			alloc.construct(first_free++, s);
		}
	}
	else if (count < size()) {
		while (first_free != elements + count) alloc.destroy(--first_free);
	}
}
template<typename T>
Vec<T>::Vec(const std::initializer_list<T>& lst) {
	auto newdata = alloc_n_copy(lst.begin(), lst.end());
	elements = newdata.first;
	first_free = cap = newdata.second;
}
template<typename T>
Vec<T>::Vec(Vec<T>&& s) noexcept :elements(s.elements), first_free(s.first_free), cap(s.cap) {
	s.elements = s.first_free = s.cap = nullptr;
}

16.1.3 模板参数

  • 模板参数名的可用范围为 其声明之后 至 模板声明或定义结束之前
  • 模板参数隐藏外层作用域中声明的相同名字
  • 参数名不能被重新定义

模板声明时必须包含模板参数, 声明和定义中使用typename 后的名字可以不同

使用类的类型成员:

默认通过作用域运算符访问的名字不是类型, 是静态成员, 如vec模板中的alloc

template<typename T> std::allocator<T> Vec<T>::alloc = std::allocator<T>();

如果希望使用一个模板类型参数的类型成员, 则必须使用typename 明确

template<typename T>
typename T::value_type top(const T& c) {
	if (!c.empty()) return c.back();
	else return typename T::value_type();
}

当希望通知编译器一个名字表示类型时, 必须使用typename, 而不能使用class.

// 这个需要练习, 琢磨

默认模板实参:

template<typename T, typename F=less<T>()>  // less<T> less<int> 比较函数
bool compare(const T& v1, const T& v2, F f) {
	if (f(v1, v2)) return -1;
	if (f(v2, v1)) return 1;
	return 0;
}

模板默认实参与类模板:

无论何时使用一个类模板(类作用于内 可以不用) , 都必须在模板名后接上尖括号.

template<typename T = int> class Numbers { // 默认为int类型  typename 也可以用class
public:
	Numbers(T v = 0) :val(v) {};
private:
	T val;
};

int main(int argc, char** argv) {
	Numbers<long double> lots;
	Numbers<> aaa; // 默认int
}

16.1.4 成员模板

类内是模板的成员函数叫成员模板, 不能是虚函数. 成员模板 有自己的参数类型

template<typename T>
class NewCall {
	template<typename TT> void funInC(TT& a) {
		//xx
	}
	template<typename TT> void funInC2(TT&);
};

template<typename T>  // 需要类 类型参数, 同时需要成员模板 类型参数
template<typename WW>  // 此处的类型参数可以与 类内定义的类型参数不同
void NewCall<T>::funInC2(WW& w) {
	//xx
};

实例化与成员模板: 编译器会根据传入的类 类型推断 模板类的类型, 会根据成员函数传入的参数推断 成员模板 的类型.

16.1.5 控制实例化

当模板被使用时才会进行实例化.  显式实例化

extern template declaration;  // 实例化声明
template declaration; // 实例化定义
// 如
extern template class Blob<string>; // 声明
template int compare(const int&, const int&); // 定义

当编译器遇到extern声明时, 不会在本文件中实例化代码, 并且承诺在程序其他位置有该实例化的一个非extern声明/定义. 可以有多个extern声明, 必须只有一个定义.

extern 必须出现在任何使用模板之前, 否则会自动实例化.

实例化定义 会实例化所有成员.

可能模板会被实例化的几种情况

  • 声明一个类模板的指针和引用,不会引起类模板的实例化,因为没有必要知道该类的定义。
  • 定义一个类类型的对象时需要该类的定义,因此类模板会被实例化。
  • 在使用sizeof()时,因为需要计算对象的大小,编译器必须根据类型将其实例化出来
  • new 类模板被实例化
  • 引用类模板的成员会导致类模板被编译器实例化。
  • 需要注意的是,类模板的成员函数本身也是一个模板。标准C++要求这样的成员函数只有在被调用或者取地址的时候,才被实例化

16.1.6 效率和灵活性

//16.28 练习题  参考 https://github.com/pezy/CppPrimer/tree/master/ch16

16.2 模版实参推断

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

const转换:  非const的引用/指针 可以传递给一个const的

template<typename T> T fobj(T, T);
template<typename T> T fref(const T&, const T&);

string s1("aaaaaa");
const string s2("bbbb");
fobj(s1, s2);  // s2 的const被忽略
fref(s1, s2);  // s1 非const转为const

数组或函数指针: 如果形参不是引用类型,  则数组/函数类型的实参可以应用正常的指针转换, 即 数组实参转换为一个指向首元素的指针, 函数实参转换为一个指向该函数类型的指针.

template<typename T> T fobj(T, T);
template<typename T> T fref(const T&, const T&);

int a[10], b[11];
fobj(a, b); // a[10] -> int*, b[11]-> int*  fobj(int*, int*)
fref(a, b); // 错误的 引用类型的形参时, 实参不转换 a[10], b[11],不是一种类型

将实参传递给带模板类型的函数形参时, 只有const转换, 数组和函数到指针的转换.

如果函数参数类型不是模板参数, 则对实参进行正常的类型转换.

// 16.34
template<typename T> int compare2(const T&, const T&);

compare2("hi", "world"); // 不合法, "hi" 转化为const char[3], "world" 转化为const char[6] 两者类型不同
compare2("bye", "daa"); // 合法, 都转化为const char(&)[4]

16.2.2 函数模板显示实参

在函数名的后面跟上<类型> 可以显式声明模板实参 如:

template<typename T>
bool compare(const T& v1, const T& v2) {
	if (v1<v2) return -1;
	if (v2<v1) return 1;
	return 0;
}
// 使用
auto a = compare<std::string>("helo", "world");

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

当出现 需要返回值类型, 但是在没确定参数类型时 无法确定返回值类型的 情况时, 需要使用尾置指明返回值类型:

template<typename It>
auto fcn(It beg, It end) -> decltype(*beg) {
	return *beg;
}

尾置返回 允许我们在参数列表之后 声明返回类型.

上面的代码只能返回元素的引用,  如果想返回一个元素的拷贝 则需要使用下面的代码

template<typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type {
	return *beg;
}

remove_reference 为标准库函数, 其作用是 脱去引用类型的 引用部分, 其中的type 则返回脱去引用部分后剩余的类型.

如: remove_reference<int&>::type  的值为int.

类似remove_reference<T>::type 的函数还有:

 16.2.4 函数指针和实参推断

可以通过函数指针 实例化 函数模板. 重载的函数需要明确究竟是哪个版本的模板.

template<typename T> int compare(const T&, const T&);
// 函数重载
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));

// 使用
int (*pf1)(const int&, const int&) = compare; // T = int
func(compare<int>); // 调用重载的func, 但是需要指明compare到底是哪个? 是string版本还是int版本

当参数是一个  函数模板实例的地址  时, 程序必须满足 对每个模板参数,能唯一确定其类型或值

16.2.5 模板实参推断和引用

从左值引用函数参数推断类型: 

template<typename T> void f1(T&) 需要传入的类型 必须是一个左值. 如果传入的const int, 则T就是const int

template<typename T>void f2(const T&) 可以传入任何参数包括一个右值. 此时会忽略掉顶层const. 如f2(ci) 如果ci 是const int的, 则T 仍然是int的

template<typename T>void f3(T&&) 右值推断和左值相同

引用折叠和右值引用参数:

两种列外:

  1. 当我们将一个左值(如i)传递给函数的右值引用参数(如f3), 且此右值引用指向模板类型参数(如T&&)时, 编译器推断模板类型参数为实参(i)的左值引用类型. 所以f3(i) 中 T的类型为int&
  2. 引用会折叠. 如 X& &, X& &&, X&& & 都会折叠成类型X&,  X&& && 折叠成 X&&

引用折叠只能应用于简介创建的引用的引用, 如果类型别名或模板参数.

16.2.6 std::move

static_cast 可以显式的将一个左值转换为一个右值引用. 

template<typename T>
typename remove_reference<T>::type&& move(T&& t) {
	return static_cast<typename remove_reference<T>::type&&>(t);
}
// 当move传入的是一个左值时, 就变成string&& move(string &t)
// static_cast<string&&>(t) t的类型为string&, cast 将一个左值引用转化为右值引用

16.2.7 转发

可以利用 模板参数 是右值引用的特性, 保留 函数的参数的所有属性, 即: 如果一个函数参数是指向模板类型参数的右值引用(T&&), 它对应的实参的const属性和左值/右值属性将得到保持. 当用于一个指向模板参数类型的右值引用函数参数(T&&)时, forward 会保持实参类型的所有细节.

template<typename F, typename T1, typename T2>
void flip(F f, T1&& t1, T2&& t2) {
	f(std::forward<T2>(t2), std::forward<T1>(t1));
}
// 此种定义可以完美保存t1, t2的属性, 并实现反转

16.3 重载与模版

函数模板可以被另一个模板或普通非模板函数重载.

重载后的匹配规则:

  1. 对于一个调用, 其候选函数包括所有模板参数推断成功的函数模板实例
  2. 候选的函数模板总是可行的, 因为模板实参推断会排除任何不可行的模板.
  3. 可行函数(模板和非模板)按类型转换来排序
  4. 如果有一个函数提供比任何其他函数都更好的匹配, 则选择此函数.如果有多个函数提供同样好的匹配, 则:
  • 如果同样好的函数中只有一个是非模板函数, 则选择此函数.
  • 如果同样好的函数中没有非模板函数, 而有多个函数模板, 且其中一个模板比其他模板更特例化, 则选择此模板
  • 否则, 此调用有歧义

16.4 可变参数模版

可以接收可变数目参数的模板函数或模板类. 可变数目的参数 -- 参数包. 符号 ...  省略号,

typename... T

两种参数包:

  • 模板参数包 -- 零或多个 模板参数
  • 函数参数包 -- 零或多个函数参数
template<typename T, typename ... Args>
void foo(const T& t, const Args& ... rest) {}

sizeof... 运算符

当我们需要知道包中有多少元素时, 可以使用sizeof... 运算符

template<typename T, typename ... Args>
void foo(const T& t, const Args& ... rest) {
	cout << sizeof...(Args) << endl;
}

16.4.1 编写可变参数函数模板

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

template<typename T>
ostream& print(ostream& os, const T& t) { return os << t; }
template<typename T, typename ... Args>
ostream &print(ostream &os, const T &t, const Args& ... rest) {
	os << t << ", ";
	return print(os, rest...);
// 此处, 每次解析rest中的第一个实参 绑定到t, 剩余的部分 继续作为rest 绑定到形参中的rest上
}

16.4.2 包扩展

即: 模板参数包/模板函数参数包 的解开操作,  如上面的print可变参数模板函数, 当传入

print(cout, i, s, 42) 时, 模板函数将会被实例化成: 

print(ostream&, const int&, const string&, const int&);

当需要调用函数对包内元素逐个执行函数的操作时, 需要:

template <typename ... Args>
ostream& errorMsg(ostream& os, const Args& ... rest) {
	return print(os, debug_rep(rest)...)
	// debug_rep 也是一个模板函数, 其接受一个参数, 并打印输出参数的内容
}

16.4.3 转发参数包

转发时 定义形参为 右值版本

template<typename ... Args>
inline void StrVec::emplace_back(Args&& ... args) {
	chk_n_alloc();
	alloc.construct(first_free++, std::forward<Args>(args)...);
}

16.5 模版特性化

当特例化一个函数模板时, 必须为原模板中的每个模板参数提供实参. 还应在template周免跟上<>. 也就是说 此处特例化定义时 使用的类型 必须是前面定义的模板的类型相匹配.

// 特例化一个compare模板
template<>
int compare(const char* const& p1, const char* const& p2) { return strcmp(p1, p2); }

   template后面应该跟的typename,用于指定模板中使用的类型, 但是因为是特例化的, 本身指定了模板中使用的类型, 所以, template后面只跟<> 而未定义类型.

const char* const & 一个指向const char的const指针的引用

函数重载与模板特例化:

一个特例化版本 本质上是一个实例, 而非函数名的一个重载版本.

模板及其特例化版本应该声明在同一个头文件中, 所有同名模板的声明应该放在前面, 然后是这些模板的特例化版本. 否则编译器很难发现没有特例化声明的错误.

类模板特例化:

类模板部分特例化:

部分特例化的类 仍然是模板类

特例化成员而不是类: 只特例化成员函数而非整个模板.

template<typename T> struct Foo2 {
	Foo2(const T &t = T()):mem(t) {}
	void Bar();
	T mem;
};
template<>
void Foo2<int>::Bar(){}  // 特例化Foo<int>的成员Bar
// 特例化以后 当使用int以外的类型实例化Foo时, 跟其他情况一样
// 当使用int类型实例化Foo时, 除了Bar其余跟其他一样



这篇关于C++ 小白 学习记录16的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程