Loading... > C++模板中有一条重要的规则,”匹配失败不是错误” 通过SFINAE,能够使得模板更为”精准”,即使的一些模板函数、模板类在实例化时使用通用的版本,这样就大大增加了模板设计的灵活性 # 一、对于重载的函数或者函数模板的选择 **对于重载的函数或者函数模板的选择上,编译器内部有一个自己的规则,并不是简单粗暴的对函数就优先选择,对函数模板就靠后选择**。 举个简单的例子:函数模板被普通函数重载 ```cpp #include <iostream> #include <vector> using namespace std; template <typename T> void myfunc1(const T& t) { cout << "myfunc1函数模板执行了" << endl; } void myfunc1(int tmpvalue) { cout << "myfunc1函数执行了" << endl; } template <typename T> void myfunc2(const T& t) { cout << "myfunc2函数模板执行了" << endl; } void myfunc2(unsigned int tmpvalue) { cout << "myfunc2函数执行了" << endl; } template <typename T> void myfunc3(const T& t) { cout << "myfunc3函数模板执行了" << endl; } void myfunc3(unsigned int tmpvalue) { cout << "myfunc3函数执行了" << endl; } int main() { //case1: myfunc1(15); //普通函数会被执行,15编译器一般会认为是一个int类型的有符号数(int) myfunc1函数执行了 //case2: //这里会调用函数模板,因为匹配的非模板函数定义为void myfunc2(unsigned int tmpvalue), //还需要进行类型转换,**将unsigned int转为int类型,所以系统不如优先匹配函数模板** myfunc2(15); //15会被推导为int类型 这里会调用函数模板 myfunc2函数模板执行了 //case3: myfunc3(15U);//myfunc3函数执行了 因为此时已经有void myfunc3(unsigned int tmpvalue) myfunc3函数执行了 return 0; } ``` 普通函数和函数模板都满足条件的话,**编译器会优先选择函数模板,但并不是简单粗暴的就选择普通函数。** ## 二、替换失败并不是一个错误(SFINAE) > SFINAE:Substitution Failure Is Not An Error,翻译成中文是“**替换失败并不是一个错误**” SFINAE看成是**C++语言的一种特性或者说一种模板设计中要遵循的重要原则**,非常重要 ## 1、SFINAE特性是针对“函数模板重载”而言 针对于函数模板而言,当用一个具体类型替换函数模板的参数时,可能会产生意想不到的问题 : 比如产生一些毫无意义甚至是看起来语法上有错误的代码,对于这些代码,编译器并不一定报错,有可能是忽略。 编译器认为这个函数模板不匹配针对本次的函数调用,就当这个函数模板不存在一样。转而去选择其他更匹配的函数或者函数模板,这就是所谓的“替换失败并不是 一个错误”说法的由来。 SFINAE特性:我(编译器)虽然看不出你(实例化了的模板)的对错(错误一般指无效的类型,无效的表达式等),但是我能决定是否选择你, 当我觉得不合适的时候,我虽然不说你错,但是我忽略你(而不会选择你); ### 举一个简单的例子 ```cpp #include <iostream> #include <vector> using namespace std; template <typename T> //编译器会替换函数中声明的部分,函数体不会去替换 typename T::size_type mydouble(const T& t) //typename int::size_type mydouble(const int& t) ; { cout << "mydouble函数模板执行了" << endl; return t[0] * 2; } int main() { mydouble(15); return 0; } ``` 会出现如下报错,如果模板无法实例化为匹配的类型的话的话 ```cpp **$ g++ 4_1.cpp -o 4_1 4_1.cpp: In function ‘int main()’: 4_1.cpp:97:21: error: no matching function for call to ‘mydouble(int)’ 97 | _nmsp2::mydouble(15); | ^ 4_1.cpp:51:24: note: candidate: ‘template<class T> typename T::size_type mydouble(const T&)’ 51 | typename T::size_type mydouble(const T& t) //typename int::size_type mydouble(const int& t) ; | ^~~~~~~~ 4_1.cpp:51:24: note: template argument deduction/substitution failed: 4_1.cpp: In substitution of ‘template<class T> typename T::size_type mydouble(const T&) [with T = int]’: 4_1.cpp:97:21: required from here 4_1.cpp:51:24: error: ‘int’ is not a class, struct, or union type** ``` 总结: 这里主要是会产生两种语法报错: 1. `'**error: no matching function for call to ‘mydouble(int)’ 未找到匹配的重载函数**`,编译器尝试使用int替换T 但是替换之后是失败的,所以会忽略这个模板,但又没找到其他合适的mydouble版本。 Note: 如果是普通函数形参不匹配的话,报的错误是,无法将参数从” xx”转为”xxx”,没有被忽略的待遇。 2. 模板实例化的话会去实例化typename int::size\_type mydouble(const int& t) `但是int::size_type int下面并没有size_type 所以模板尝试实例化后就会去报错` 解决办法: ```cpp #include <iostream> #include <vector> using namespace std; template <typename T> typename T::size_type mydouble2(const T& t) //typename int::size_type mydouble(const int& t) ; { cout << "mydouble2函数模板执行了" << endl; return t[0] * 2; } int mydouble2(int i) { cout << "mydouble2函数执行了" << endl; return i * 2; } int main() { mydouble2(15);//可以正常通过编译,因为已经单独定义了int mydouble2(int i)函数 return 0; }} ``` 单独再定义一个int mydouble2(int i)函数之后就不会再编译报错 函数模板和普通函数都存在时,优先调用普通函数 ### 适合于函数模板,反而不适合普通函数 ```cpp #include <iostream> #include <vector> using namespace std; template <typename T> typename T::size_type mydouble3(const T& t) { cout << "mydouble3函数模板执行了" << endl; return t[0] * 2; } int main() { //myvec 是vector类型 vector类型是有size_type类型的 //typename vector::size_type mydouble3(const vector& t) 这种是可以编译通过的 vector<int> myvec; myvec.push_back(15); cout << mydouble3(myvec) << endl; return 0; } ``` typename vector::size\_type mydouble3(const vector& t) 这种是可以编译通过的,就不会报错 # 三、enable\_if的使用 > 基础认识:C++11新标准中引入的类模板(结构模板)。使用体现了C++编译器的SFINAE特性。 定位为一个helper模板(助手模板),用于辅助其他模板的设计,表现一种:编译器的分支逻辑(`编译期就可以确定走哪条分支`)。 偏特化完全可以理解成一种条件分支语句。 ## 1、enable\_if用于函数模板中 > 典型应用是作为函数模板的返回类型。 enable\_if的源码: ```cpp 在泛化版本中Ty已经有默认值void了,特化版本也可以使用 //STRUCT TEMPLATE enable_if template <bool _Test, class _Ty = void> //泛化版本 注意这里的第一个参数是bool类型 struct enable_if {}; // no member "type" when !_Test //只有当第一个bool的参数为true时,才会有type这个类型 类模板数量上的偏特化 template <class _Ty> //偏特化版本:怎么理解:只有这个偏特化版本存在,才存在一个名字叫做type的类型别名(类型) struct enable_if<true, _Ty> { // type is _Ty for _Test //std::cout <<__LINE__<<endl;//无法通过编译 using type = _Ty; }; ``` 偏特化完全可以理解为一种条件分支语句。 > 2、举个例子 ```cpp #include <iostream> #include <vector> using namespace std; template <typename T> struct MEB { using type = T;//只是起了一个别名 }; int main() { MEB<int>::type abc = 15; //MEB<int>::type 就代表int类型 //case1:因为此时的bool类型为true 所以使用的是偏特化版本 std::enable_if< (3 > 2)>::type* mypoint1 = nullptr; //等价于void *mypoint1 = nullptr //case2:因为此时的bool类型为false 所以使用的是泛化版本 但是泛化版本中没有type这个别名类型 所以最终导致编译出错 //std::enable_if< (3 < 2)>::type* mypoint1 = nullptr; return 0; } ``` struct MEB { //private:别名这里要为public 否则也会编译报错 using type = T;//只是起了一个别名 }; ### 辅助其它函数模板 ```cpp #include <iostream> #include <vector> using namespace std; //这个场景enable_if_t 用在了返回值上,而且返回值是T 不再是void,所以funceb必须要有一个返回值 template <typename T> std::enable_if_t<(sizeof(T) > 2), T> funceb() //int funceb(){} { T myt = {}; return myt; } //这个场景enable_if_t 用在了返回值上,而且返回值使用的是默认的void 因为被缺省了 template <typename T> //typename std::enable_if<(sizeof(T) > 2)>::type funceb1(){} // void funceb(){} //在C++14中可以省略typename 和 ::type可以等价于下面这种写法 std::enable_if_t<(sizeof(T) > 2)> funceb1(){} int main() { funceb<int>();//等价于调用的函数为int funceb(){} //funceb<char>();//会编译报错 funceb1<int>();// void funceb(){} return 0; } ``` 为什么这个地方没有泛化版本也能编译通过? template <typename T> typename std::enable\_if<(sizeof(T) > 2)>::type funceb1(){} // void funceb(){} 可以等价于下面这种写法 template <typename T> std::enable\_if\_t<(sizeof(T) > 2)> funceb1(){} ## 2、enable\_if用于类模板中 ### 2.1、给类型模板定义一个别名(别名模板) std::is\_convertible,配合enable\_if使用,C++11引入。两个模板参数分别是From 和To:**用于判断能否从某个类型隐式的转换到另外一个类型**。返回一个bool值——true或者false。 ```cpp #include <iostream> #include <vector> using namespace std; int main() { cout << "string=>float:" << std::is_convertible<string, float>::value << endl; cout << "float=>int:" << std::is_convertible<float, int>::value << endl; return 0; } $ ./3_4_2 string=>float:0 float=>int:1 ``` is\_convertible用于隐式类型转换 ### 2.3、应用场景 ```cpp #include <iostream> #include <string> #include <vector> using namespace std; class Human { public: //构造函数模板 template<typename T> Human(T&& tmpname) : m_sname(std::forward<T>(tmpname)) { cout << "Human(T&& tmpname)执行" << endl; } //拷贝构造函数 Human(const Human& th) : m_sname(th.m_sname) { cout << "Human(const Human& th)拷贝构造函数执行" << endl; } //移动构造函数 Human(Human&& th) : m_sname(std::move(th.m_sname)) { cout << "Human(Human&& th)移动构造函数执行" << endl; } private: string m_sname; }; int main() { string sname = "ZhangSan"; Human myhuman1(sname); Human myhuman3(myhuman1); //实际编译器去调用了构造函数模板,而不是调用了拷贝构造函数。 return 0; } ``` 问题: 编译器只会去调用构造函数模板,而不是调用了拷贝构造函数。 解决办法:使用enable\_if ```cpp #include <iostream> #include <string> #include <vector> using namespace std; //给类型模板定义一个别名(别名模板) template <typename T> using StrProcType = std::enable_if_t<std::is_convertible<T, std::string>::value >; class Human { public: //构造函数模板 template< typename T, //typename U = std::enable_if_t<std::is_convertible<T, std::string>::value > //如果T能够成功转换成std::string类型,那么typename = std::enable_if_t<std::is_convertible<T, std::string>::value > 等价于 //typename U = void //因为这里U用不到,所以可以进行省略,语法上没问题 直接写为typename = std::enable_if_t<std::is_convertible<T, std::string>::value >//标准库中,这种用法很常见 typename = StrProcType<T>//或者typename U = StrProcType<T> > Human(T&& tmpname) : m_sname(std::forward<T>(tmpname)) { cout << "Human(T&& tmpname)执行" << endl; } //拷贝构造函数 Human(const Human& th) : m_sname(th.m_sname) { cout << "Human(const Human& th)拷贝构造函数执行" << endl; } //移动构造函数 Human(Human&& th) : m_sname(std::move(th.m_sname)) { cout << "Human(Human&& th)移动构造函数执行" << endl; } private: string m_sname; }; int main() { string sname = "ZhangSan"; Human myhuman1(sname); Human myhuman3(myhuman1); //实际编译器去调用了构造函数模板,而不是调用了拷贝构造函数。 return 0; } 执行结果: $ ./3_4_3 Human(T&& tmpname)执行 Human(const Human& th)拷贝构造函数执行 ``` typename U = std::enable\_if\_t<std::is\_convertible<T, std::string>::value> **当模板参数不使用的时候就可以省略不写,这个地方也可以写为** typename = std::enable\_if\_t<std::is\_convertible<T, std::string>::value> 最后修改:2025 年 07 月 01 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏