C++模板

你好旅行者!欢迎来到cyt的练功房。本篇文章来记录一下C的模板编程与SFINAE现象。众所周知,C的模板编程是一个非常神奇的东西,它可以在编译期时重新生成代码,从而达到类型泛型的效果,但其实他的效果又比泛型强大太多,虽然在工程中很少用到,但在看懂stl源码的过程中尤其重要,本文讲以我浅薄的认知来讲一下我理解下的C++模板元编程。

一、普通模板类和模板函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template<typename T>
bool aequalsb(T a, T b) {
return a == b;
}
template<typename T>
class Entity {
public:
T res;
Entity(T a) {
res = a;
std::cout << res << std::endl;
}
};
int main() {
int ai = 3, bi = 4;
double ad = 3, bd = 3;
string as = "as", bs = "as";
bool res0 = aequalsb(ai, bi);
bool res1 = aequalsb(ad, bd);
bool res2 = aequalsb(as, bs);
std::cout << "int的比较结果是:"<<res0 <<"\n" << "double的比较结果是:" << res1 << "\n" << "string的比较结果是:" << res2 << "\n" << std::endl;
Entity<int> e0(ai);
Entity<double> e1(ad);
Entity<string> e2(as);
}

以上代码可以在编译的时候编译器可以根据给定的值的类型进行代码的重新生成,使文件中生成对应不同类型输入的代码。这类似于其他语言中的泛型。但模板元编程的功能远不止于此。还可以进行神奇的操作,比如

1
2
3
4
5
6
7
8
9
10
template<typename T,typename... U>
void println(T t, U ...u) {
if constexpr (sizeof...(U) == 0) {
cout << t << endl;
}
else {
cout << t << ",";
println(u...);
}
}

可以使用以上代码来进行任意输入的打印。虽然涉及到了参数包等新东西,或许将来会更新,具体使用准则可以参考cppreference。

模板的全特化和偏特化

所谓偏特化与全特化其实就是在选择哪一个模板的优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T,typename U>
void aequalsb(T a, U b) {
cout << "普通模板来咯" << endl;
}
template<>
void aequalsb(int a, int b) {
cout << "全特化来咯" << endl;
}
template<typename T>
void aequalsb(T a, double b) {
cout << "偏特化模板来咯" << endl;
}
aequalsb(1.03, 1.06);//偏特化
aequalsb(3, 5);//全特化
aequalsb(11, 1.06);//偏特化
aequalsb(1.06, 11);//普通模板

可以看到当模板进行了特化的时候,模板会根据传入形参的类型获得优先级,优先级可分为模板参数与形参类型完全相同,模板参数与形参类型部分相同,模板参数与形参类型完全没有相同

SFINAE

SFINAE全称为"Substitution Failure is not an error",其实也是因为模板的优先级会导致模板匹配到错误的类型中,从而导致代码出现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T,unsigned N>
std::size_t len(T(&)[N]) {
return N;
}
template<typename T>
typename T::size_type len(T const& t) {
return t.size();
}
int len(...) {
return 0;
}
class Myclass {
public:
using size_type = unsigned int;
};

我们定义这样的三个函数和三个类,正如上文所说,不同的形参类型会导致不同的模板在编译期的特化所以当我们在main函数中运行以下代码时

1
2
3
4
5
6
int a[10];
vector<int> b(8);
int c = 3;
cout<<len(a)<<endl;
cout << (len(b)) << endl;
cout << len(c) << endl;

会得到的输出结果是10 8 0。

1
2
Myclass d;
cout<<len(d)<<endl;

那当我们输入的类型是这个类的时候会发生什么呢? 它有size_type属性,却没有size()方法。在我们看来,他是不是应该走最基础的返回0的那个函数,但其实它会走入第二个也就是容器的那个模板参数,这就是SFINAE,代表模板匹配错误。

enable_if

为了解决上面那个SFINAE的问题,C++11中引入了enable_if关键字,它可以让模板来判断这个形参是否可以匹配这个类。
我们改一下上面那个模板

1
2
3
4
template<typename T,typename T2 = typename enable_if<!is_same<T, Myclass>::value>::type>
typename T::size_type len(T const& t) {
return t.size();
}

这样只有在T不等于Myclass时,T2才有意义,模板才能被实例化。所以会走入那个保底的len。从此在运行

1
2
Myclass d;
cout<<len(d)<<endl;

enable_if其实是一个模板萃取中的一个方法,其有两个参数<bool ,typename>,当第一个参数是1时,返回类名,类名也可以不输入,默认是void,可以接受任意函数,当第一个参数是0时,没有返回值,也就不会实例化啦~~~。现在运行以上代码就不会报错了,会输出一个0。其实应该写在下面也可以,但是我不会。以上我们就理解了SFINAE的出现情况和如何避免出现SFINAE

enable_if重定义问题

以上你是不是以为就万事大吉了呢?其实并不是,让我们看看以下的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Myclass {
public:
using size_type = unsigned int;
};
class Myclass1 {
using size_type = unsigned int;
};
int len(...) {
return 0;
}
template<typename T,typename enable_if<!is_same<T, Myclass>::value>::type>
typename T::size_type len(T const& t) {
return t.size();
}
template<typename T, typename enable_if<!is_same<T, Myclass1>::value>::type>
typename T::size_type len(T const& t) {
return t.size();
}
int main(){
Myclass d;
Myclass1 e;
cout << len(d) << endl;
cout << len(e) << endl;
}

当运行以上代码时,你认为输出什么呢?是不是利索当然的认为是两个0呢?但其实他会报错,真是太难过了,这究竟是为什么呢?
原来啊,不管怎么说typename enable_if<!is_same<T, Myclass>::value>::type都是一个类型,你可以把它们想象成int和double

1
2
3
4
5
6
7
8
template<typename T,typename T2 = double>
typename T::size_type len(T const& t) {
return t.size();
}
template<typename T, typename T2 = int>
typename T::size_type len(T const& t) {
return t.size();
}

这样的代码显然是有二义性的,编译器怎么会知道这个模板要调用哪一个模板来重载呢?那么我们怎么解决这个问题呢。为了更方便的得出结论,我将非号去掉了,当是myclass1时返回1,myclass返回0。

1
2
3
4
5
6
7
8
template<typename T,  typename enable_if<is_same<T, Myclass>::value>::type* = nullptr>
int len(T const& t) {
return 0;
}
template<typename T, typename enable_if<is_same<T, Myclass1>::value>::type* = nullptr>
int len(T const& t) {
return 1;
}

这是为什么呢,因为我们传入了一个非类型的参数,我们知道type的类型是一个void,我们只是不知道他的值,而nullptr又可以接受参数所有的所以借助SFINAE在这一长串存在时就不会存在二义性的问题了。

使用搜索:谷歌必应百度