Template 2nd阅读摘要(1)

1.延迟推导
Cont中的模板形参可以是不完整类型,因为其内部成员elems为指针类型。

template<typename T>
class Cont {
private:
T* elems;
public:};

但是一旦加了foo()成员函数:

template<typename T>
class Cont {
private:
T* elems;
public:typename std::conditional<std::is_move_constructible<T>::value,
T&&,
T&
>::type
foo();
};

struct Node
{
std::string value;
Cont<Node> next; // only possible if Cont accepts incomplete types
};

foo返回值中对T是否支持转移语义做了判断,is_move_constructible要求T为完整类型,此时若外部使用Cont,就会报编译错误。通过将foo()改为带独立模板形参的模板成员函数,可以达到延迟加载的效果。

template<typename T>
class Cont {
private:
T* elems;
public:
template<typename D = T>
typename std::conditional<std::is_move_constructible<D>::value,
T&&,
T&
>::type
foo();
};

如上,foo()带独立的模板形参D,默认参数为T,将对D的替换试错延迟到foo函数调用时。
2. 构造函数模板
构造函数模板同样导致禁用默认模板类的默认构造函数。为了保证默认构造函数可用,要显示声明默认构造函数为default。
3. 模板形参不能指定默认参数的场景:
Template 2nd阅读摘要(1)_第1张图片
4. SFINAE是为了适应C++中的函数重载而有的一种技术,目的是为了在存在多个重载版本的函数时,通过做多次的替换试错,找到最佳匹配的版本。
5. 非类型的模板参数必须满足一个约束:在编译期间能就能够获取到值。若是在运行期间才能够获取到的值,如局部变量地址,则与模板的定义不想匹配,模板实例化是在编译期间完成的。
6. 对模板费类型参数赋值时,接收隐式转换,或定义了constexpr的类型转换表达式才允许不同类型的常量赋值。子类指针赋值给父类指针是不允许的。
7. 模板模板形参只能用class关键字,不能用typename。模板的模板模板类型的实参中的默认参数是被忽略的,如:

#include 
// declares in namespace std:
// template>
// class list;
template<typename T1, typename T2,
template<typename> class Cont> // Cont expects one parameter
class Rel {};
Rel<int, double, std::list> rel; // ERROR before C++17: std::list has more than one template parameter

但当前模板的模板模板形参的默认参数在编译时是会参与评估的。
8. 函数模板实例化后的覆盖替换规则,同名的模板函数不会覆盖普通函数。
Template 2nd阅读摘要(1)_第2张图片
9. 对变化参数模板中参数类型的操作成为Pattern,其可以是衍生新的类型,如T*…,或是基于类型的表达式,如f(T)。该表达式可能非常复杂。在类型pack展开时候,该pattern会重复地逐个作用在包内的每个类型上。
10. 类模板的友元类
类模板的友元声明有如下规则:
1) 如果是类模板的部分示例作为友元,则在类模板内的友元声明之前必须以声明,并在当前范围内可见;普通的非模板类做为友元无此约束;
2) 友元类不能有定义,只能是声明;

template <typename T1>
class B1;

template <typename Ty>
class A
{
	friend class B1<Ty>; // 部分实例作为友元,必须要有之前的声明在,否则编译报错;
	
	friend class B2;     //普通类作为友元,不需要之前存在类声明,可以是首个声明
	
	template <typename T2>
	friend class B3;     //首次声明的类模板作为友元
};

不同模板形参的实例化类,可以认为是完全不同的类,类模板中声明为私有的函数是不能够直接访问的,若要访问必须声明为友元,如:

template <typename T>
class A
{
public:
	template<typename T2>
	void swap(const A<T2>& other)
	{
		other.test();
		std::cout << "swap" << std::endl;
	}

	// 若无此友元声明则会报编译错误,
	// 因为A和A是两个完全不同的模板类,不可访问私有声明的成员函数
	template <typename T3>
	friend class A;

private: 
	void test() const
	{
		std::cout << "test()" << std::endl;
	}
};

void testFriendClass
{
	A<int> a;
	A<double> b;
	a.swap(b);
}

12. 类的友元函数
特化的模板函数,作为友元声明时,其后一定要带尖括号,这是语法上的要求。如果实例化时能够从函数的形参类型中推导出模板形参类型,函数模板的实参列表也要保留"<>"。

template <typename T> void f(T t);

class BL
{
	friend void f<int>(int);
	friend void f<>(int);
};
  1. 依赖名称查找会导致类模板的实例化
    测试代码如下:
template <typename T>
class C
{
	static_assert(std::is_same<T, int>::value, "got here");
	friend void f1();
	friend void f2(C<T>* const);
};

void testADL()
{
	// 如果只有此句是不会触发类模板实例化的
	C<float>* ptr_c = nullptr;
	// 该符号查找不到,因为其没有入参,adl也就无法生效
	f1();
	// 该处会引起C模板的实例化,借助ADL能够在C的类作用域内找到该符号。
	f2(p);
}
  1. current instantiation
    在类定义内的类型名称作为注入类名,若为类模板,则类名为与形参类型一致的模板类名,称之为current instiation。对于与当前形参不一致的,或是依赖类型,则未为未知特化,如下代码会报错:
template <typename T>
class Node
{
	using Type = T;
	Node* next;  // Node即为current instiation,等价于Node,T为模板实参
	Node<Type>* privous;
	// 未知特化,这样的类型引入是不允许的
	Node<T*> parent;
};
  1. 模板的依赖名
    对类型名称而言,编译器通常会假设这个类型名是不引用模板,除非通过template关键字指定,以表示该关键后面的template-id,否则就会将尖括号当做大于或这小于号进行表达式解析。如下代码:
template<typename T>
class Shell {
public:
	template<int N>
	class In {
	public:
		template<int M>
		class Deep {
		public:
			virtual void f();
		};
	};
};
template<typename T, int N>
class Weird {
public:
	void case1(
		// 通过template关键字指定类型名称为依赖名称
		typename Shell<T>::template In<N>::template Deep<N>* p) {
		p->template Deep<N>::f(); // inhibit virtual call
	}
	void case2(
		typename Shell<T>::template In<N>::template Deep<N>& p) {
		p.template Deep<N>::f(); // inhibit virtual call
	}
};
  1. using符号导入
    使用using关键字,可将命名空间内或类范围内的变量或类型导入到当前作用域,就像是在当前作用域定义了同名符号一样。常见用法是将基类中定义的符号引入子类,将不可访问的符号变为可访问。示例如下:
template <typename T>
class BXT
{
public:
	using Mystery = T;
	template <typename U>
	struct Magic;
		
	// 成员函数
	void f();
};

template <typename T>
class DXTT : private BXT<T> //私有继承,除了using导入的符号外,其余基类符号不可访问
{
public:
	using BXT<T>::f;
	using typename BXT<T>::Mystery;
	Mystery* p;
};
  1. 继承关系下的变量或类型查找
    模板类可以作为继承体系中的基类,也可继承自其它类。若基类为模板类由可分为依赖型基类和非依赖型基类, 非依赖型基类是说在模板实参未知的情况下,也能够确定该类型的完整定义,否则就是依赖型基类。二者在子类实现时,对于变量或类型的查找规则上是有差异的。对于非依赖型基类,在模板解析时,子类空间也会作为查找范围,若模板类型定义与基类中的类型定义同名冲突则优先使用基类中的类型定义,此时子类若引用了基类中的变量也是能够查找到的。但对于依赖型基类则不然,子类中出现的非限定性变量采取就近原则,仅在当前子类范围内做变量查找,不会查找基类,这类问题就会延迟到额子类模板实例化的时候。针对此种情况,若明确引用基类中定义的名称,则通过this->或加基类命名限定的方式来强制类型查找,从而将符号查找提前至模板解析阶段。示例代码如下:
    对于current instatiation中的限定名称查找,C++标准中规定名称的查找首先在current instatantiation的作用域和非依赖型基类中查找,类似非限定名称查找。若找到该名称,则该限定名称引用current instantation中的成员,并且不是依赖型名称。若为知道该名称,并且该类型有任何依赖型的模板基类,则该限定名称引用为未知特化中的成员。示例如下:
class NonDep {
public:
	using Type = int;
};
template<typename T>
class Dep {
public:
	using OtherType = T;
};
template<typename T>
class DepBase : public NonDep, public Dep<T> {
public:
	void f() {
		typename DepBase<T>::Type t; // finds NonDep::Type;
									 // typename keyword is optional
		typename DepBase<T>::OtherType* ot; // finds nothing; DepBase::OtherType is a member of an unknown specialization
	}
};

对依赖类模板而言,实现上应用的接口或变量在模板形参未确定之前存在性是未知额,只有到了实例化阶段,模板实参确定后,才能能够zh知道名称是否存在。
18. 类模板实例化
类模板采用延迟实例化,或懒惰实例化的策略,在不需要知道模板类的完全定义时,仅做部分实例化(partical institantation)。隐式实例化模板类,在实例化过程中会对类定义内的如下内容做实例化校验:
1) 类内的union或class定义、typedef声明和成员变量定义等;
2) 对成员函数仅做函数声明的校验,包括成员函数入参和返回值的合法性校验;函数提内的类型合法性仅在调用该成员函数时校验;但是对于虚函数例外,其在类实例化时,就会对函数体内的类型做合法性校验。因为对于虚函数调用机制而言,构造函数虚表时,要求虚函数一定是作为可链接实体存在的。
测试验证的代码如下:

template <typename T>
class InstantationTest
{
public: 
	typedef T::ValuType VT; //#1 类模板实例化时就会校验	
	void candidate()
	{
		T::ValueType v;  //#2 普通函数:类模板实例化时不做校验,仅在函数调用时校验
	}

	void candidate(T::ValueType t); //#3 类模板实例化时会做校验
	{
		T::ValueType v;  //#4 类模板实例化时不做校验,仅在函数调用时校验
	}

	virtual void candiate1()
	{
		T::ValueType v; //#5类模板实例化时就会做函数体内的类型合法性校验,
	}

	virtual void candidate(T t = "aaa") //#5.1 默认参数赋值也是不做合法性校验的,仅有在使用调用该函数,并且用到了默认参数才校验;
	{
	}
private: 
	T::ValueType value;  //#6 类模板实例化时就会校验
};

void check()
{
	InstantationTest<int>* ptr_it; //#7 对类模板做部分实例化,不需要知道其完整的类型定义
	InstantationTest<int> it;      //#8 需要能够访问到实例化类的完整定义
}
  1. Two-phase Lookup
    早期的编译器采用模板实例化时,一次解析的策略,在名称的引用上很容易引入问题。如下实例代码,func的引用应当是模板定义之前的函数,结果却是后面一个:
#include 

void func(void*) { std::puts("The call resolves to void*") ;}

template<typename T> void g(T x)
{
	func(0);
}

void func(int) { std::puts("The call resolves to int"); }

int main()
{
	// 在对模板函数g的调用处模板函数实例化,并做名称查找,导致func的引用出错。
	g(3.14);
}

为了解决此类问题,在名称绑定上,做了依赖名称和非依赖名称的划分。按照c++标准,名称包含如下三种:

  1. 模板名称或模板声明中的模板参数名称;
  2. 依赖模板参数的名称;
  3. 模板定义内范围内的名称;
    第一类和第三类都是非依赖名称,是在模板定义时就已绑定的,并在后续模板的实例化中保持绑定,模板实例化后不再做名称的查找。而依赖名称则不是在模板定义中绑定的,此类名称要在模板实例化中查找。调用依赖函数名称的函数,在函数的调用处,函数名称会与模板定义处可见的函数集合相绑定。通过ADL查找的其他重载函数,包括在模板定义处的和函数实例化处,都会加入到该名称绑定集合中。对于处在模板定义和模板实例化之间的重载函数只能通过ADL查找找到。 依据上述规则,典型实例代码如下:
#include 

void func(long) { std::puts("func(long)"); }

template <typename T> void meow(T t) {
	func(t);
}

void func(int) { std::puts("func(int)"); }

namespace Kitty {
struct Peppermint {};
void func(Peppermint) { std::puts("Kitty::func(Kitty::Peppermint)"); }
}

int main() {
	meow(1729);  // T为int类型,为全局作用域,所以仅做普通查找,从而匹配到到模板定义之前的函数func(long)
	Kitty::Peppermint pepper;
	meow(pepper);// T为Kitty::Peppermint类型,通过ADL查找匹配到模板定义之后的函数名称func(Peppermint)
}

参考如下文章
二阶段名称查找的MSVC支持
20. 参数依赖查找(ADL)
ADL查找,即在函数符号的查找中会扩大名称查找范围做形参所在作用域的符号查找,模板查找还包括模板形参。适用的2个条件:
a. 函数名称必须是非限定名称;
b. 表达式必须为函数调用,否则不会触发ADL;
若参数为int的全局作用域,则匹配不到特定作用域内的符号名称。示例代码如下:

namespace Deep
{
	struct A {};
	void func(const A& a) //#1
	{
		std::cout << "This is func(A);" << std::endl;
	}

	void func(const std::string& str)  #2
	{
		std::cout << "This is func(std::string)" << std::endl;
	}
}


void main()
{
	Deep::A a;
	func(a); //匹配#1位置的符号,通过a所在作用域查找到匹配符号;

	std::string str("test");
	func(str); //报错无此符号;

	system("pause");
}

ADL
21. 函数模板的几种形参上的差别

template <typename T>
void TestParamType(T t); //#1

template <typename T>
void TestParamType(T& t); //#2

template <typename T>
void TestParamType(T&& t);//#3

#1-非引用类型,有点类似传参时参数复制的意思,所以若入参为引用类型会退化为非引用类型,若为数组则退化为指针;
#2-左值引用类型,语义上允许函数内部修改,仅接受左值,不接受右值。数组类型不会退化。
#3-前向引用(右值引用)类型,既可以接收左值又可以接受右值,类型推导时,会根据实参类型做引用折叠。数组类型同样不会退化。
完美转发与之相关,目的是为了间接函数调用时,实现参数的准确透传, 透传时保留参数的引用类型。在标注库中make_shared等,都是应用的实例。
22. SFINAE中的Immediate Context
在函数模板的替换中,存在即时上下文的概念,只有在即时上下文中中出现无效类型、表达式或符号歧义才会仅踢出候选,不编译错误。非即时上下文则直接报错,书中整理了非即时上下文:Template 2nd阅读摘要(1)_第3张图片
最常见的就前两个,类模板的定义和函数模板的函数体。示例代码如下:

template<typename T>
class Array {
public:
	using iterator = T*;
};

template<typename T>
void f(Array<T>::iterator first, Array<T>::iterator last);
template<typename T>
void f(T*, T*);

int main()
{
	f<int&>(0, 0);	// ERROR: substituting int& for T in the first functiontemplate
}					// instantiates Array, which then fails

在替换过程中,第一个候选,校验Array中是否存在iterator成员类型时,要首先实例化模板类,Array,由于该实例化模板中存在无效,int&*引用类型的指针,其出现在非即时上下文中所以直接报编译错误,而不仅仅时踢出候选。
该技术起初是用于多个重载版本的函数模板筛选,现在既可以用于函数模板筛选,也可用于类模板特化版本的筛选。
23. auto类型推导
表达式中auto的推导可等价同形式的函数模板函数形参的推导。在推导中也有auto,auto&和auto&&三种形式,对单独的auto是不能够对应引用类型的,因为推导时的类型退化。同时auto关键字的使用位置也有约束:不能出现在函数模板形参位置或紧跟类型说明符之后作为声明的一部分。

template<typename T> struct X { T const m; };
auto const N = 400u; // OK: constant of type unsigned int
auto* gp = (void*)nullptr; // OK: gp has type void*
auto const S::*pm = &X<int>::m; // OK: pm has type int const X::*
X<auto> xa = X<int>(); // ERROR: auto in template argument
int const auto::*pm2 = &X<int>::m; // ERROR: auto is part of the “declarator”
  1. 函数模板的全特化在类型能够通过函数形参推导的情况下可以不必显示指定,但全特化函数的实现不应该全部放在头文件中,因为只能特化一次,可以将其声明和实现分开,头文件中仅保留声明,实现移至cpp文件中。
  2. 类模板的全特化版本与通用模板比一定有联系,其可以定义自己独有的成员函数,二者唯一的联系是类名一致。类模板的静态成员和成员函数也是可以单独特化的,但要注意的是一旦函数或成员对某一类型的模板形参特化后就不能在对改类型的类模板进行全特化。
  3. 对于输入为多个模板形参的类模板返回为bool类型的,称之为之为类型断言,是一种特殊形式的Traits。该种类型可用作函数的参数,以达到类型不同,执行不同的重载版本的效果,也叫Tag dispatch。
  4. 为了便于使用,通常会对模板类模板重定义的类型或静态常量定义别名,以便于访问。类型的别名定义使用using,常量定义通常使用constexpr。实例代码如下:
template <typename T>
struct RemoveRef
{
	using Type = T;
}

template <typename T>
struct RemoveRef<T&>
{
	using Type = T;
}

template <typename T>
using RemoveRefT = typename RemoveRef<T>::Type;

/*******************/
template <typename T>
struct IsInt { static const bool value = false; };
template<>
struct IsInt<int> { static const bool value = true; };

template <typename T>
constexpr bool IsIntV = IsInt<T>::value;
  1. declval实现上是一个返回某类型右值引用的模板函数,所以其实际上并未调用构造函数构造临时变量,从而避免引入不必要的约束。其经常用于推导某个表达式的结果类型。如下示例:
#include 
template<typename T1, typename T2>
struct PlusResultT {
	//通估declval和decltype推导两类型实例加和的结果类型
	using Type = decltype(std::declval<T1>() + std::declval<T2>());
};
template<typename T1, typename T2>
using PlusResult = typename PlusResultT<T1, T2>::Type;

你可能感兴趣的:(C++模板)