深入模板原理
函数模板,类模板的实参推导
-
函数模板的实参推导
函数模板的实参推导是发生在名字查找之后,和重载决议之前,如果函数模板推导失败,编译器不会直接报错,而是把这个函数从重载集中删除
template<typename T,typename U> void foo(T,U){}; //#1 template<typename T> void foo(T,T){};//#2 void foo(float,int){}; //#3 foo(1,1.0f);//call #1 由于推导#2失败 //1. 编译器看到名字为foo的调用 //2. 编译器找到所有foo的名字的函数和函数模板 3个都符合 //3. 编译器对于每个函数和函数模板都尝试通过实参(1,1.0f)来推断模板实参 //#1:T = int ,U = float //#2: 推导失败 2被移除重载集 //4. 编译器对当前的重载集 #1和#3 进行重载决议 选择#1 //5. 编译器对#1 进行替换(实例化)(T,U) -》 (int,float)
-
类模板的实参推导
实参推导只考虑主模板 不考虑模板特化,如果主模板实参推导失败,编译器直接报错
template<typename T,typename U> struct S { S(T a,U b ){};}; //#1 template<typename T> struct S<T,float> { S(T a,T b ){};}; //#2 template<> struct S<int,int> { S(int a,int b ){};}; //#3 S s(1,1.0f);//call #2 通过主模板构造函数推导出T=int U = float,推导结果拿去匹配特化,匹配最佳特化为#2 //假设通过#2来做模板实参推导,推导失败 把1,1.0f 带入#2的构造函数就会推导失败,所以也反证了编译器不是用模板特化去推导实参 //1. 编译器看到变量s的定义 //2. 编译器通过名字查找找到S的类或者类模板,这里应该只能找到一个,否则会重定义错误 //3. 对于S的主模板#1,编译器尝试通过构造函数的的实参1,1.0f,去推导模板实参,T = int, U = float //4.编译器根据模板的实参去匹配最佳的特化 选择#2 //5. 编译器对#2进行替换,完成隐式实例化
-
特化选择
在所有的模板实参都确定了(可以是显示指定的,可以退推导的 或者从默认实参中获取的),当所有的模板实参都确定了后,编译器就需要在主模板和所有的特化中选择其中一个来进行实例化
- 对于每个模板特化,先判断能不能匹配实例化
- 如果只有一个模板特化能匹配模板实参,那么就选择这个特化
- 如果多个模板特化都可以匹配,那么通过特化的偏序关系来判断哪个模板匹配程度更高,匹配程度最高的特化被选择,
- 如果没有任何特化可以匹配,那么主模板就会被选中
不严谨的说,A的特化比B高,A的特化接受的参数是B接受参的子集。严谨的说对于2个特化A和B,编译器会首先把A和B转换成2个虚构的函数模板FA和 FB,然后模板特化的形参就被转换成函数的形参。
template<typename T,typename U,typename ...Args> struct S{}; template<typename T,typename U> struct S<T,U>{}; //#A template<typename T> struct S<T,int>{}; //#B //#A -> template<typename T,typename U> void FA(S<T,U>) //#B -> template<typename T> void FB(S<T,int>) //这样转换后,就转变成了函数模板的重载决议规则(归一化了)
-
模板的偏序规则
对于2个函数模板,怎么判断谁的特化程度更高,也是个代入推导的过程
template<typename T> void foo(T){}; //#1 template<typename T> void foo(T*){}; //#2 template<typename T> void foo(const T*){};#3 const int*p; foo(p); //对于#1 和 #2的偏序关系 //1. 尝试用#2代入推导#1,假设给#2传入实参U //#1 变成void(T) #2变成void(U*) ,用#2代入#1 T = U* 推导ok T = U* //2. 尝试用#1 代入推导#2 //#1 变成void(U) #2变成void(T*) 用#1代入推导#2 T* = U 推导失败 //综上 #2的特化程度比#1高
函数模板的重载集是偏序集,直观的说就是集合中并不是所有的元素都可以拿来对比的,模板中并不是所有的模板都可以比较谁的特化程度更高
template <typename T> void foo(T,T*){}; //#1 template <typename T> void foo(T,int*); //#2 //1. 尝试用#2 代入推导1,假设给#2传入实参U //#(U,int*) = (T,T*) 推导失败,如果T被推导成U 那么(U,U*) = (U,int*) 显然失败的 //2. 尝试用#1 代入推导#2 假设给#1传入实参U //(U,U*) = (T,int*) 显然失败的 //无法推导的情况下,编译器无法完成重载决议,就会抛出“ambiguous” 错误。其实如果相互都可以推导成功,也是无法比较的,同样对比编译器无法完成重载决议。
-
重载和特化的关系
函数模板的每个重载都是主模板,在重载决议的时候 只考虑主模板。模板的特化不在重载集的范围内。对于一个函数调用先进行重载决议,确定使用哪个主模板,然后在考虑要不要使用它的特化。所以先进行重载决议后进行选择特化。做重载决议的时候,特化根本不在编译器考虑范围内
template <typename T> void foo(T){}; //#1 template <> void foo(int*){}; //#2 template <typename T> void foo(T*){}; //#3 foo((int*)(0)); //call #3 由于#2是#1的特化,重载决议的时候压根看不到#2 //这样#2就会被调用,调换位置后#2变成#3的特化 template <typename T> void foo(T){}; //#1 template <typename T> void foo(T*){}; //#3 template <> void foo(int*){}; //#2
-
SFINAE substitution Is Not An Error
替换失败并不是一个错误,替换失败指的用实参替换模板形参后,在模板的“立即上下文”中,呈现出“非良构”(ill-formed)
- 一个类型或者表达式(ill-formed)指的代码违背了语法或者语义的规则
- 立即上下文简单的说是模板声明中看到的内容
- “不是一个错误”,如果函数模板在替换失败后,替换失败直接从重载集中移除,编译器会尝试其他重载并不会抛出一个错误。类模板和变量模板偏特化替换失败,这个特化从特化集中移除,编译器继续尝试其他特化,并不会报错。
template<typename T> typename T::value_type foo(T t) { //int::value为ill-formed 这里会替换失败 (也就是这个错误不会被直接报错) return t::value_type; //int::value为ill-formed 但是这里不是立即上下文 编译报错 } foo(1);
SFIINAE在函数模板中
template<typename T> void foo(T) {} //#1 template<typename T> void foo(T*) {} //#2 template<typename T> typename T::value_type foo(T) {} //#3 foo(1); //#3会发生替换失败 int::value_type SFINAE #1h和#2中重载选择#1 foo(new int); //#3会发生替换失败 int::value_type SFINAE #1h和#2中重载选择#2 foo<int&&>(1); //#2,#3发生替换失败 SFINAE #1 选中
SFIINAE在类模板偏特化中
template<typename T,typename U> struct S {}; template<typename T> struct S <T,typename T::value_type> {}; S<int,int>(); //#2 SFINAE 选择#1 S<std::true_type,bool>(); //#2 S<std::true_type,int>(); //#1
回顾下实例化过程
- 首先名字查找,编译器首先根据标识符查找同名的模板
- 如果函数模板,会找到多个模板
- 如果变量或者类模板,找到唯一的主模板
- 确定所有的实参
- 对于类模板或者变量模板,主模板推导失败了就报错
- 如果函数模板,如果推导失败了,就从重载集中移除
- 对于函数模板要进行重载决议,重载决议只考虑主模板,采用偏序规则,SFINAE发生作用
- 特化选择,对于类模板,SFINAE发生作用,对于函数模板,由于只有全特化,直接匹配就可以了。
- 对于最终选择的模板进行替换操作,生成真实的代码,放入POI(point of instantiation),生成代码插入的位置
应用TMP
enable_if实现
template<bool,typename T = void>
struct enable_if:std::type_identity<T>{};
template<typename T>
struct enable_if<false,T>{};
template<typename T> enable_if< std::is_integral_v<T> >::type foo(T) {}; //#1
template<typename T> enable_if< std::is_floating_point_v<T> >::type foo(T) {}; //#2
foo(1); //匹配 #1 匹配#2的过程中会发生SFINAE enable_if<false,float>::type 出错
foo(1.0f);//匹配#2 匹配#1过程会发生SFINAE
//利用SFINAE 我们有了基于逻辑控制函数重载集的能力
//通过类模板控制函数重载
template<typename T>
struct S {
template<typename U> static enable_if<std::is_same_v<T,int>>::type foo(U) {}; //#1
template<typename U> static enable_if<!std::is_same_v<T,int>>::type foo(U) {};//#2
};
S<int>::foo(1);
//编译出错#2 由于enable_if<false>::type 虽然是foo的“立即上下文”,但是不是S的立即上下文
//所以如果想变成S的立即上下文中,要推迟enable_if的计算到实例化foo的时候
template<typename ...Args>
struct always_true:std::true_type{}
//添加个关于U的表达式 合取表达式,所以就会延迟到foo的实例化的时候在求值enable_if
template<typename U> static enable_if<always_true<U> && std::is_same_v<T,int>>::type foo(U) {};
void_t
template<typename ...>
using void_t = void
template<typename,typename = void> struct has_type_member : false_type {};
template<typename T> struct has_type_member<T,void_t<typename T::type>> : true_type {};
std::cout<<has_type_member<int><<std::endl; // false
std::cout<<has_type_member<true_type><<std::endl; // true
std::cout<<has_type_member<type_identity<int>><<std::endl; // true
//实现类似py中has_attr的效果,内部是否有type的member
不求值表达式
c++中4个运算操作符,操作时不会求值的,typeid,sizeof,noexcept,decltype,这4个操作符只对操作数的编译期进行访问
template<typename T> enable_if<is_integral_v<T>,int> foo(T) {};
template<typename T> enable_if<is_floating_point_v<T>,float> foo(T) {};
template<typename T> struct {decltype(foo<T>(??)) value_;}; //期望返回foo<T>(??)函数的返回值类型
//但是这里我们编译器产生不了一个变量啊
//可以通过declval来产生一个假象的变量 ,只有申明没有定义,只能用在不求值上下文中
template<typename T> add_rvalue_reference_t<T> decval() noexcept; //这里没有定义
//有了decval 就可以“伪造”一个变量传给foo
template<typename T> struct {decltype(foo<T>(decval<T>())) value_;};
add_reference
template<typename T> struct add_lvalue_reference:type_identify<T&>{};
template<typename T> struct add_rvalue_reference:type_identify<T&&>{}
//问题 void 没有引用类型
//add_lvalue_reference<void>::type 编译出错
namespace helper {
template<typename T> type_identify<T&> try_add_lvalue_reference(int);
template<typename T> type_identify<T> try_add_lvalue_reference(...);
};
template<typename T> struct add_lvalue_reference :decltype(helper::try_add_lvalue_reference<T>(0)) {};
//当T可以加上左值引用就添加 利用SFINAE
std::cout<<is_same_v<char&,add_lvalue_reference<char>> <<std::endl;
std::cout<<is_same_v<void,add_lvalue_reference<void>> <<std::endl;
is_copy_assignable
//判断一个类型是否可以拷贝赋值
struct S{ S& operator=(S const &) = delete;};
std::cout<<is_copy_assignable(int)<<std::endl; //1
std::cout<<is_copy_assignable(true_type)<<std::endl; //1
std::cout<<is_copy_assignable(S)<<std::endl; //0
//假设中有 a = b 这样的表达式,先写出来在说 ,看看是不是良构表达式 同样利用SFINAE
template<typename T> using copy_assign_t = decltype(declval<T&>() = declval<const T&>());
template<typename T,typename = void> struct is_copy_assignable : false_type {};
template<typename T> struct is_copy_assignable<T,void_t<copy_assign_t<T>>> : true_type {};
标准库中tuple的实现
int i= 1;
auto t = tuple(0,i,'2',3.0f,4ll,std::string("five"));
template<typename... Args> struct tuple {tuple(Args...) {};}; //主模板
//特化实现了递归的继承,最终匹配到主模板 递归终止
template<typename T,typename... Args> struct tuple:tunple<Args...> {
tuple(T v,Args... params):value_(v),tunple<Args...>(params...) {
}
T value_;
};
//怎么去读tuple中n个元素
is_same_v<tuple_element_t<2,decltype(t)>,int);
//
template<unsigned N,typename T>
struct tuple_element;
template<unsigned N,typename T,typename ...Args>
struct tuple_element<N,tuple<T,Args...>> : tuple_element<N-1,tuple<Args...>>{};
template<typename T,typename ...Args>
struct tuple_element<0,tuple<T,Args...>> : type_identify<T>{using _tuple_type = tuple<T,Args...>};
// 比如tuple_element[3] 此时_tuple_type的用处就是从元素3以后所有元素构成的tuple的类型
template<unsigned N,typename...Args>
tuple_element_t<N,tuple<Args...>>& get(tuple<Args...>&t){
using _tuple_type = typename tuple_element<N,tuple<Args...>>::_tuple_type;
return static_cast<_tuple_type&>(t).value; //返回是左值引用 可以修改的
}
cout<<get<1>(t)<<endl; //1
cout<<get<5>(t)<<endl; //five
网友评论