mirror of
https://github.com/wuye9036/CppTemplateTutorial.git
synced 2024-03-22 13:11:16 +08:00
Update ReadMe.md
Added section 'SFINAE'
This commit is contained in:
parent
02687cfcf3
commit
250c73b42a
460
ReadMe.md
460
ReadMe.md
|
@ -2079,7 +2079,454 @@ void foo(){
|
|||
* A和B都与模板实参无法匹配,所以使用原型,调用`CustomDiv`
|
||||
|
||||
###3.2 后悔药:SFINAE
|
||||
###3.3 实战单元:获得类型的属性——类型萃取(Type Traits)
|
||||
|
||||
考虑下面这个函数模板:
|
||||
|
||||
``` C++
|
||||
template <typename T, typename U>
|
||||
void foo(T t, typename U::type u) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
到本节为止,我们所有的例子都保证了一旦咱们敲定了模板参数中 `T` 和 `U`,函数参变量 `t` 和 `u` 的类型都是成立的,比如下面这样:
|
||||
|
||||
``` C++
|
||||
struct X {
|
||||
typedef float type;
|
||||
};
|
||||
|
||||
template <typename T, typename U>
|
||||
void foo(T t, typename U::type u) {
|
||||
// ...
|
||||
}
|
||||
|
||||
void callFoo() {
|
||||
foo<int, X>(5, 5.0); // T == int, typename U::type == X::type == int
|
||||
}
|
||||
```
|
||||
|
||||
那么这里有一个可能都不算是问题的问题 —— 对于下面的代码,你认为它会提示怎么样的错误:
|
||||
|
||||
```C++
|
||||
struct X {
|
||||
typedef float type;
|
||||
};
|
||||
|
||||
struct Y {
|
||||
typedef float type2;
|
||||
};
|
||||
|
||||
template <typename T, typename U>
|
||||
void foo(T t, typename U::type u) {
|
||||
// ...
|
||||
}
|
||||
|
||||
void callFoo() {
|
||||
foo<int, X>(5, 5.0); // T == int, typename U::type == X::type == int
|
||||
foo<int, Y>(5, 5.0); // ???
|
||||
}
|
||||
```
|
||||
|
||||
这个时候你也许会说:啊,这个简单,`Y` 没有 `type` 这个成员自然会出错啦!嗯,这个时候咱们来看看Clang给出的结果:
|
||||
|
||||
```
|
||||
error: no matching function for call to 'foo'
|
||||
foo<int, Y>(5, 5.0); // ???
|
||||
^~~~~~~~~~~
|
||||
note: candidate template ignored: substitution failure [with T = int, U = Y]: no type named 'type' in 'Y'
|
||||
void foo(T t, typename U::type u) {
|
||||
```
|
||||
|
||||
完整翻译过来就是,直接的出错原因是没有匹配的 `foo` 函数,间接原因是尝试用 `[T = int, U = y]` 做类型替换的时候失败了,所以这个函数模板就被忽略了。等等,不是出错,而是被忽略了?那么也就是说,只要有别的能匹配的类型兜着,编译器就无视这里的失败了?
|
||||
|
||||
银河火箭队的阿喵说,就是这样。不信邪的朋友可以试试下面的代码:
|
||||
|
||||
```C++
|
||||
struct X {
|
||||
typedef float type;
|
||||
};
|
||||
|
||||
struct Y {
|
||||
typedef float type2;
|
||||
};
|
||||
|
||||
template <typename T, typename U>
|
||||
void foo(T t, typename U::type u) {
|
||||
// ...
|
||||
}
|
||||
|
||||
template <typename T, typename U>
|
||||
void foo(T t, typename U::type2 u) {
|
||||
// ...
|
||||
}
|
||||
void callFoo() {
|
||||
foo<int, X>(5, 5.0); // T == int, typename U::type == X::type == int
|
||||
foo<int, Y>( 1, 1.0 ); // ???
|
||||
}
|
||||
```
|
||||
|
||||
这下相信编译器真的是不关心替换失败了吧。我们管这种只要有正确的候选,就无视替换失败的做法为SFINAE。
|
||||
|
||||
我们不用纠结这个词的发音,它来自于 Substitution failure is not an error 的首字母缩写。这一句之乎者也般难懂的话,由之乎者 —— 啊,不,Substitution,Failure和Error三个词构成。
|
||||
|
||||
我们从最简单的词“Error”开始理解。Error就是一般意义上的编译错误。一旦出现编译错误,大家都知道,编译器就会中止编译,并且停止接下来的代码生成和链接等后续活动。
|
||||
|
||||
其次,我们再说“Failure”。很多时候光看字面意思,很多人会把 Failure 和 Error 等同起来。但是实际上Failure很多场合下只是一个中性词。比如我们看下面这个虚构的例子就知道这两者的区别了。
|
||||
|
||||
假设我们有一个语法分析器,其中某一个规则需要匹配一个token,它可以是标识符,字面量或者是字符串,那么我们会有下面的代码:
|
||||
|
||||
```C++
|
||||
switch(token)
|
||||
{
|
||||
case IDENTIFIER:
|
||||
// do something
|
||||
break;
|
||||
case LITERAL_NUMBER:
|
||||
// do something
|
||||
break;
|
||||
case LITERAL_STRING:
|
||||
// do something
|
||||
break;
|
||||
default:
|
||||
throw WrongToken(token);
|
||||
}
|
||||
```
|
||||
假如我们当前的token是 `LITERAL_STRING` 的时候,那么第一步它在匹配 `IDENTIFIER` 时,我们可以认为它失败(failure)了,但是它在第三步就会匹配上,所以它并不是一个错误。
|
||||
|
||||
但是如果这个token既不是标识符、也不是数字字面量、也不是字符串字面量,而且我们的语法规定除了这三类值以外其他统统都是非法的时,我们才认为它是一个error。
|
||||
|
||||
大家所熟知的函数重载也是如此。比如说下面这个例子:
|
||||
|
||||
```C++
|
||||
struct A {};
|
||||
struct B: public A {};
|
||||
struct C {};
|
||||
|
||||
void foo(A const&) {}
|
||||
void foo(B const&) {}
|
||||
|
||||
void callFoo() {
|
||||
foo( A() );
|
||||
foo( B() );
|
||||
foo( C() );
|
||||
}
|
||||
```
|
||||
|
||||
那么 `foo( A() )` 虽然匹配 `foo(B const&)` 会失败,但是它起码能匹配 `foo(A const&)`,所以它是正确的;`foo( B() )` 能同时匹配两个函数原型,但是 `foo(B const&)` 要更好一些,因此它选择了这个原型。而 `foo( C() );` 因为两个函数都匹配失败(Failure)了,所以它找不到相应的原型,这时才会爆出一个编译器错误(Error)。
|
||||
|
||||
所以到这里我们就明白了,在很多情况下,Failure is not an error。编译器在遇到Failure的时候,往往还需要尝试其他的可能性。
|
||||
|
||||
好,现在我们把最后一个词,Substitution,加入到我们的字典中。现在这句话的意思就是说,我们要把 Failure is not an error 的概念,推广到Substitution阶段。
|
||||
|
||||
所谓substitution,就是将函数模板中的形参,替换成实参的过程。概念很简洁但是实现却颇多细节,所以C++标准中对这一概念的解释比较拗口。它分别指出了以下几点:
|
||||
|
||||
* 什么时候函数模板会发生实参 替代(Substitute) 形参的行为;
|
||||
|
||||
* 什么样的行为被称作 Substitution;
|
||||
|
||||
* 什么样的行为不可以被称作 Substitution Failure —— 他们叫SFINAE error。
|
||||
|
||||
我们在此不再详述,有兴趣的同学可以参照 http://en.cppreference.com/w/cpp/language/sfinae ,这是标准的一个精炼版本。这里我们简单的解释一下。
|
||||
|
||||
考虑我们有这么个函数签名:
|
||||
|
||||
```C++
|
||||
template <
|
||||
typename T0,
|
||||
// 一大坨其他模板参数
|
||||
typename U = /* 和前面T有关的一大坨 */
|
||||
>
|
||||
RType /* 和模板参数有关的一大坨 */
|
||||
functionName (
|
||||
PType0 /* PType0 是和模板参数有关的一大坨 */,
|
||||
PType1 /* PType1 是和模板参数有关的一大坨 */,
|
||||
// ... 其他参数
|
||||
) {
|
||||
// 实现,和模板参数有关的一大坨
|
||||
}
|
||||
```
|
||||
|
||||
那么,在这个函数模板被实例化的时候,所有函数签名上的“和模板参数有关的一大坨”被推导出具体类型的过程,就是替换。一个更具体的例子来解释上面的“一大坨”:
|
||||
|
||||
```C++
|
||||
template <
|
||||
typename T,
|
||||
typenname U = typename vector<T>::iterator // 1
|
||||
>
|
||||
typename vector<T>::value_type // 1
|
||||
foo(
|
||||
T*, // 1
|
||||
T&, // 1
|
||||
typename T::internal_type, // 1
|
||||
typename add_reference<T>::type, // 1
|
||||
int // 这里都不需要 substitution
|
||||
)
|
||||
{
|
||||
// 整个实现部分,都没有 substitution。这个很关键。
|
||||
}
|
||||
```
|
||||
|
||||
所有标记为 `1` 的部分,都是需要替换的部分,而它们在替换过程中的失败(failure),就称之为替换失败(substitution failure)。
|
||||
|
||||
下面的代码是提供了一些替换成功和替换失败的示例:
|
||||
|
||||
```C++
|
||||
struct X {
|
||||
typedef int type;
|
||||
};
|
||||
|
||||
struct Y {
|
||||
typedef int type2;
|
||||
};
|
||||
|
||||
template <typename T> void foo(typename T::type); // Foo0
|
||||
template <typename T> void foo(typename T::type2); // Foo1
|
||||
template <typename T> void foo(T); // Foo2
|
||||
|
||||
void callFoo() {
|
||||
foo<X>(5); // Foo0: Succeed, Foo1: Failed, Foo2: Failed
|
||||
foo<Y>(10); // Foo0: Failed, Foo1: Succeed, Foo2: Failed
|
||||
foo<int>(15); // Foo0: Failed, Foo1: Failed, Foo2: Succeed
|
||||
}
|
||||
```
|
||||
|
||||
在这个例子中,当我们指定 `foo<Y>` 的时候,substitution就开始工作了,而且会同时工作在三个不同的 `foo` 签名上。如果我们仅仅因为 `Y` 没有 `type`,匹配 `Foo0` 失败了,就宣布代码有错,中止编译,那显然是武断的。因为 `Foo1` 是可以被正确替换的,我们也希望 `Foo1` 成为 `foo<Y>` 的原型。
|
||||
|
||||
std/boost库中的 `enable_if` 是 SFINAE 最直接也是最主要的应用。所以我们通过下面 `enable_if` 的例子,来深入理解一下 SFINAE 在模板编程中的作用。
|
||||
|
||||
假设我们有两个不同类型的计数器(counter),一种是普通的整数类型,另外一种是一个复杂对象,它从接口 `ICounter` 继承,这个接口有一个成员叫做increase实现计数功能。现在,我们想把这两种类型的counter封装一个统一的调用:inc_counter。那么,我们直觉会简单粗暴的写出下面的代码:
|
||||
|
||||
```C++
|
||||
struct ICounter {
|
||||
virtual void increase() = 0;
|
||||
virtual ~ICounter() {}
|
||||
};
|
||||
|
||||
struct Counter: public ICounter {
|
||||
void increase() override {
|
||||
// Implements
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
void inc_counter(T& counterObj) {
|
||||
counterObj.increase();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void inc_counter(T& intTypeCounter){
|
||||
++intTypeCounter;
|
||||
}
|
||||
|
||||
void doSomething() {
|
||||
Counter cntObj;
|
||||
uint32_t cntUI32;
|
||||
|
||||
// blah blah blah
|
||||
inc_counter(cntObj);
|
||||
inc_counter(cntUI32);
|
||||
}
|
||||
```
|
||||
|
||||
我们非常希望它展现出预期的行为。因为其实我们是知道对于任何一个调用,两个 `inc_counter` 只有一个是能够编译正确的。“有且唯一”,我们理应当期望编译器能够挑出那个唯一来。
|
||||
|
||||
可惜编译器做不到这一点。首先,它就告诉我们,这两个签名
|
||||
|
||||
```C++
|
||||
template <typename T> void inc_counter(T& counterObj);
|
||||
template <typename T> void inc_counter(T& intTypeCounter);
|
||||
```
|
||||
|
||||
其实是一模一样的。我们遇到了 `redefinition`。
|
||||
|
||||
我们看看 `enable_if` 是怎么解决这个问题的。我们通过 `enable_if` 这个 `T` 对于不同的实例做个限定:
|
||||
|
||||
```C++
|
||||
template <typename T> void inc_counter(
|
||||
T& counterObj,
|
||||
typename std::enable_if<
|
||||
is_base_of<T, ICounter>::value
|
||||
>::type* = nullptr );
|
||||
|
||||
template <typename T> void inc_counter(
|
||||
T& counterInt,
|
||||
typename std::enable_if<
|
||||
std::is_integral<T>::value
|
||||
>::type* = nullptr );
|
||||
```
|
||||
|
||||
然后我们解释一下,这个 `enable_if` 是怎么工作的,语法为什么这么丑:
|
||||
|
||||
首先,替换(substitution)只有在推断函数类型的时候,才会起作用。推断函数类型需要参数的类型,所以, `typename std::enable_if<std::is_integral<T>::value>::type` 这么一长串代码,就是为了让 `enable_if` 参与到函数类型中;
|
||||
|
||||
其次, `is_integral<T>::value` 返回一个布尔类型的编译器常数,告诉我们它是或者不是一个 `integral type`,`enable_if<C>` 的作用就是,如果这个 `C` 值为 `True`,那么 `enable_if<C>::type` 就会被推断成一个 `void` 或者是别的什么类型,让整个函数匹配后的类型变成 `void inc_counter<int>(int & counterInt, void* dummy = nullptr);` 如果这个值为 `False` ,那么 `enable_if<false>` 这个特化形式中,压根就没有这个 `::type`,于是替换就失败了。和我们之前的例子中一样,这个函数原型就不会被产生出来。
|
||||
|
||||
所以我们能保证,无论对于 `int` 还是 `counter` 类型的实例,我们都只有一个函数原型通过了substitution —— 这样就保证了它的“有且唯一”,编译器也不会因为你某个替换失败而无视成功的那个实例。
|
||||
|
||||
这个例子说到了这里,熟悉C++的你,一定会站出来说我们只要把第一个签名改成:
|
||||
|
||||
```C++
|
||||
void inc_counter(ICounter& counterObj);
|
||||
```
|
||||
|
||||
就能完美解决这个问题了,根本不需要这么复杂的编译器机制。
|
||||
|
||||
嗯,你说的没错,在这里这个特性一点都没用。
|
||||
|
||||
这也提醒我们,当你觉得需要写 `enable_if` 的时候,首先要考虑到以下可能性:
|
||||
|
||||
* 重载(对模板函数)
|
||||
|
||||
* 偏特化(对模板类而言)
|
||||
|
||||
* 虚函数
|
||||
|
||||
|
||||
但是问题到了这里并没有结束。因为 `increase` 毕竟是个虚函数。假如 `Counter` 需要调用的地方实在是太多了,这个时候我们会非常期望 `increase` 不再是个虚函数以提高性能。此时我们会调整继承层级:
|
||||
|
||||
```C++
|
||||
struct ICounter {};
|
||||
struct Counter: public ICounter {
|
||||
void increase() {
|
||||
// impl
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
那么原有的 `void inc_counter(ICounter& counterObj)` 就无法再执行下去了。这个时候你可能会考虑一些变通的办法:
|
||||
|
||||
```C++
|
||||
template <typename T>
|
||||
void inc_counter(ICounter& c) {};
|
||||
|
||||
template <typename T>
|
||||
void inc_counter(T& c) { ++c; };
|
||||
|
||||
void doSomething() {
|
||||
Counter cntObj;
|
||||
uint32_t cntUI32;
|
||||
|
||||
// blah blah blah
|
||||
inc_counter(cntObj); // 1
|
||||
inc_counter(static_cast<ICounter&>(cntObj)); // 2
|
||||
inc_counter(cntUI32); // 3
|
||||
}
|
||||
```
|
||||
|
||||
对于调用 `1`,因为 `cntObj` 到 `ICounter` 是需要类型转换的,所以比 `void inc_counter(T&) [T = Counter]` 要更差一些。然后它会直接实例化后者,结果实现变成了 `++cntObj`,BOOM!
|
||||
|
||||
那么我们做 `2` 试试看?嗯,工作的很好。但是等等,我们的初衷是什么来着?不就是让 `inc_counter` 对不同的计数器类型透明吗?这不是又一夜回到解放前了?
|
||||
|
||||
所以这个时候,就能看到 `enable_if` 是如何通过 SFINAE 发挥威力的了:
|
||||
|
||||
```C++
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <cstdint>
|
||||
|
||||
struct ICounter {};
|
||||
struct Counter: public ICounter {
|
||||
void increase() {
|
||||
// impl
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T> void inc_counter(
|
||||
T& counterObj,
|
||||
typename std::enable_if<
|
||||
std::is_base_of<ICounter, T>::value
|
||||
>::type* = nullptr ){
|
||||
counterObj.increase();
|
||||
}
|
||||
|
||||
template <typename T> void inc_counter(
|
||||
T& counterInt,
|
||||
typename std::enable_if<
|
||||
std::is_integral<T>::value
|
||||
>::type* = nullptr ){
|
||||
++counterInt;
|
||||
}
|
||||
|
||||
void doSomething() {
|
||||
Counter cntObj;
|
||||
uint32_t cntUI32;
|
||||
|
||||
// blah blah blah
|
||||
inc_counter(cntObj); // OK!
|
||||
inc_counter(cntUI32); // OK!
|
||||
}
|
||||
```
|
||||
|
||||
这个代码是不是看起来有点脏脏的。眼尖的你定睛一瞧,咦, `ICounter` 不是已经空了吗,为什么我们还要用它作为基类呢?
|
||||
|
||||
这是个好问题。在本例中,我们用它来区分一个`counter`是不是继承自`ICounter`。最终目的,是希望知道 `counter` 有没有 `increase` 这个函数。
|
||||
|
||||
所以 `ICounter` 只是相当于一个标签。而于情于理这个标签都是个累赘。但是在C++11之前,我们并没有办法去写类似于:
|
||||
|
||||
```C++
|
||||
template <typename T> void foo(T& c, decltype(c.increase())* = nullptr);
|
||||
```
|
||||
|
||||
这样的函数签名,因为假如 `T` 是 `int`,那么 `c.increase()` 这个函数调用就不存在。但它又不属于Type Failure,而是一个Expression Failure,在C++11之前它会直接导致编译器出错,这并不是我们所期望的。所以我们才退而求其次,用一个类似于标签的形式来提供我们所需要的类型信息。以后的章节,后面我们会说到,这种和类型有关的信息我们可以称之为 `type traits`。
|
||||
|
||||
到了C++11,它正式提供了 Expression SFINAE,这时我们就能抛开 `ICounter` 这个无用的Tag,直接写出我们要写的东西:
|
||||
|
||||
```C++
|
||||
struct Counter {
|
||||
void increase() {
|
||||
// Implements
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
void inc_counter(T& intTypeCounter, std::decay_t<decltype(++intTypeCounter)>* = nullptr) {
|
||||
++intTypeCounter;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void inc_counter(T& counterObj, std::decay_t<decltype(counterObj.increase())>* = nullptr) {
|
||||
counterObj.increase();
|
||||
}
|
||||
|
||||
void doSomething() {
|
||||
Counter cntObj;
|
||||
uint32_t cntUI32;
|
||||
|
||||
// blah blah blah
|
||||
inc_counter(cntObj);
|
||||
inc_counter(cntUI32);
|
||||
}
|
||||
```
|
||||
|
||||
此外,还有一种情况只能使用 SFINAE,而无法使用包括继承、重载在内的任何方法,这就是Universal Reference。比如,
|
||||
|
||||
```C++
|
||||
// 这里的a是个通用引用,可以准确的处理左右值引用的问题。
|
||||
template <typename ArgT> void foo(ArgT&& a);
|
||||
```
|
||||
|
||||
加入我们要限定ArgT只能是 float 的衍生类型,那么写成下面这个样子是不对的,它实际上只能接受 float 的右值引用。
|
||||
|
||||
```C++
|
||||
void foo(float&& a);
|
||||
```
|
||||
|
||||
此时的唯一选择,就是使用Universal Reference,并增加 `enable_if` 限定类型,如下面这样:
|
||||
|
||||
```C++
|
||||
template <typename ArgT>
|
||||
void foo(
|
||||
ArgT&& a,
|
||||
typename std::enabled_if<
|
||||
is_same<ArgT, float>::value
|
||||
>::type* = nullptr
|
||||
);
|
||||
```
|
||||
|
||||
从上面这些例子可以看到,SFINAE最主要的作用,是保证编译器在泛型函数、偏特化、及一般重载函数中遴选函数原型的候选列表时不被打断。除此之外,它还有一个很重要的元编程作用就是实现部分的编译期自省和反射。
|
||||
|
||||
虽然它写起来并不直观,但是对于既没有编译器自省、也没有Concept的C++1y来说,已经是最好的选择了。
|
||||
|
||||
## 4 用模板写程序吧!骚年!
|
||||
###4.1 模板上的递归
|
||||
|
@ -2087,11 +2534,12 @@ void foo(){
|
|||
###4.3 实战单元:元编程的Fibonacci数列
|
||||
|
||||
## 5 元编程下的数据结构与算法
|
||||
###5.1 列表与数组
|
||||
###5.2 字典结构
|
||||
###5.3 “快速”排序
|
||||
###5.4 其它常用的“轮子”
|
||||
boost.mpl
|
||||
###5.1 获得类型的属性——类型萃取(Type Traits)
|
||||
###5.2 列表与数组
|
||||
###5.3 字典结构
|
||||
###5.4 “快速”排序
|
||||
###5.5 其它常用的“轮子”
|
||||
boost.hana
|
||||
|
||||
## 6 模板的进阶技巧
|
||||
###6.1 嵌入类
|
||||
|
|
Loading…
Reference in New Issue
Block a user