SFINAE :编译期的类成员函数重载

最近在写模板类的时候遇到一个问题:一个类成员函数如何针对某一特定的template<typename T> 模板参数类型,有默认的行为。

请你先想五秒钟,再接着往下看。

我想你应该和我一样,映入眼帘的第一个想法,是在这个函数里调用typeid(T)来判断T的类型,以此来让函数针对某一类型T有默认的行为:

1
2
3
if (typeid(T) == typeid(std::string)) {
// 针对 string 的默认行为
}

但是很抱歉,事与愿违,事情远远没有我们想象的那么简单。
发生了什么事呢?
让我们先来个看个完整的例子

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
26
27
28
29
30
#include <string>
#include <vector>
#include <iostream>

template<typename T>
class Printer
{
public:
Printer() { }

void DoIt(const T& t)
{
if (typeid(T) == typeid(std::string)) {
std::cout << t << std::endl;
} else {
std::cout << "I don't know how to print" << std::endl;
}
}
};

int main(int argc, char** argv)
{
Printer<std::string> p;
p.DoIt("WTF");

Printer<std::vector<char>> p2;
p2.DoIt({'W', 'T', 'F'});

return 0;
}

编译它

1
2
3
4
% clang++ SFINAE.cpp -std=c++11
SFINAE.cpp:14:23: error: invalid operands to binary expression ('ostream' (aka 'basic_ostream<char>') and 'const std::vector<char, std::allocator<char> >')
std::cout << t << std::endl;
~~~~~~~~~ ^ ~

可以看到编译器给出了没办法调用std::cout<< t << std::endl的错误,t是一个std::vector<char>类型。
看到这里你应该明白了,在上述例子的第14行中,编译器对模板使用了std::vector<char>类型进行实例化,实例化后的Printer类变成了这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Printer
{
public:
Printer() { }

void DoIt(const std::vector<char>& t)
{
if (typeid(std::vector<char>) == typeid(std::string)) {
std::cout << t << std::endl;
} else {
std::cout << "I don't know how to print" << std::endl;
}
}
};

虽然第8行的if永远都不会成立,但是编译器可没有那么聪明。他只看到了你要对 std::vector<char>类型调用std::cout进行输出,这当然是不行的。
你可能会想,要是编译器足够聪明,可以把这段永远不会执行的if分支在编译期给消灭掉,不就得了吗?对此我们暂时按下不表。先来看看在C++11标准下,要如何解决这个问题。也就是说,如何实现一个 “编译期的if”。

另一个想法映入眼帘,

我们要如何实现针对类成员函数的“偏特化”。

先把正确答案贴上,别急着关闭网页,我慢慢跟你解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Printer
{
public:
Printer() {
}

template <typename U = T>
void
DoIt(const T& t, typename std::enable_if<std::is_same<U, std::string>::value, void>::type * = nullptr)
{
std::cout << t << std::endl;
}

template <typename U = T>
void
DoIt(const T& t, typename std::enable_if<!std::is_same<U, std::string>::value, void>::type * = nullptr)
{
std::cout << "I don't know how to print" << std::endl;
}
};

先从本文的标题说起。SFINAE是「Substitution failure is not an error」的缩写。

typename

http://feihu.me/blog/2014/the-origin-and-usage-of-typename/

SFINAE

https://zhuanlan.zhihu.com/p/21314708

immediate context

https://codeday.me/en/qa/20190306/13897.html

std::enable_if_

The Curse of Productivity

1. The Prefix

在购买了这个打算用做博客的域名一年后,一篇文章没写下的我被namesilo催续费了。支付了 $7.99 有点肉疼,毕竟白放了一年什么也没干。

2. The Curse

抛开文学水平低下不说,这一年里还是有几次冲动想要写点东西的,也确实写下了一点,但都流产了。究其原因,不知从什么时候开始,我太过关注于工具所能提供的生产力,甚至大过了生产本身。生产工具的不称手导致生产需要更多的 mental energy,导致完全不想进行生产。Reddit 上 vim 板的这张图就特别贴切地描述了这个状态:

img

当然这种状态有好处也有坏处,拿写博客来说:

Cons: 这一年里的几次冲动都因为没有顺心如意的工具鸽了。这次在肉疼之后总算找到一个顺心的工具,叫做 Ulysses,才能支撑我在百忙之中(并没有)写下这篇水文(这是真的),虽然插入图片的体验并不是那么顺畅(

Pros: 这个工具也让我意识到笔记的收集和笔记的整理并不需要在同一个App上。所以打算彻底抛弃现在使用的非常 buggy 的为知笔记,切换成 Ulysses + Evernote。希望得到生产力的大跃进(好像有点不对

3. The Productivity

文章的第二段原本是叫 Curse of Productivity, 害怕有哪些单词拼错,放到 Google 搜索了一下,发现 Observer 的文章 The Curse of Productivity 描述了和我大概一样的状态。索性把文章也改成同名了。其引用了二八定律表示花仅花20%的时间就能得到80%的生产力提高,其余20%生产力的提高要花掉80%的时间,以至于因为生产力提高而省下的时间可能还不够提高生产力本身所花费的时间。对此我真是深有同感。配置VIM的时间中有80%的都花在为了提高那20%的生产力上。

文章还认为如果太高效会导致 less creative,对此我不敢苟同。我认为既然生产力的提高降低了做事情需要的 mental energy,那么也就会有更多的 mental energy 来做更加 creative 的事情。

4. The End

从立题到内容有点高开低走了哈,但是废话就写到这,希望今年可以多多产出,Consume less, create more!

你看这篇文章 —— 还是在写生产力!