TMP(2)

2021-11-07  本文已影响0人  Teech

深入模板原理

函数模板,类模板的实参推导

  1. 函数模板的实参推导

    函数模板的实参推导是发生在名字查找之后,和重载决议之前,如果函数模板推导失败,编译器不会直接报错,而是把这个函数从重载集中删除

    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)
    
  2. 类模板的实参推导

    实参推导只考虑主模板 不考虑模板特化,如果主模板实参推导失败,编译器直接报错

    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进行替换,完成隐式实例化
    
  3. 特化选择

    在所有的模板实参都确定了(可以是显示指定的,可以退推导的 或者从默认实参中获取的),当所有的模板实参都确定了后,编译器就需要在主模板和所有的特化中选择其中一个来进行实例化

    1. 对于每个模板特化,先判断能不能匹配实例化
    2. 如果只有一个模板特化能匹配模板实参,那么就选择这个特化
    3. 如果多个模板特化都可以匹配,那么通过特化的偏序关系来判断哪个模板匹配程度更高,匹配程度最高的特化被选择,
    4. 如果没有任何特化可以匹配,那么主模板就会被选中

    不严谨的说,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>)
    //这样转换后,就转变成了函数模板的重载决议规则(归一化了)
    
  4. 模板的偏序规则

    对于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” 错误。其实如果相互都可以推导成功,也是无法比较的,同样对比编译器无法完成重载决议。
    
    
  5. 重载和特化的关系

    函数模板的每个重载都是主模板,在重载决议的时候 只考虑主模板。模板的特化不在重载集的范围内。对于一个函数调用先进行重载决议,确定使用哪个主模板,然后在考虑要不要使用它的特化。所以先进行重载决议后进行选择特化。做重载决议的时候,特化根本不在编译器考虑范围内

    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
    
  6. 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
    

回顾下实例化过程

  1. 首先名字查找,编译器首先根据标识符查找同名的模板
    • 如果函数模板,会找到多个模板
    • 如果变量或者类模板,找到唯一的主模板
  2. 确定所有的实参
    • 对于类模板或者变量模板,主模板推导失败了就报错
    • 如果函数模板,如果推导失败了,就从重载集中移除
  3. 对于函数模板要进行重载决议,重载决议只考虑主模板,采用偏序规则,SFINAE发生作用
  4. 特化选择,对于类模板,SFINAE发生作用,对于函数模板,由于只有全特化,直接匹配就可以了。
  5. 对于最终选择的模板进行替换操作,生成真实的代码,放入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
上一篇下一篇

猜你喜欢

热点阅读