我们偶然间聊到了这两个要素,于是心血来潮总结了一下。
这篇文章需要一定的C++模板基础来阅读。
Trait 类型提取
基础推理
众所周知,除了 RTTI 能够动态确定 Class 类型外,基础类型在运行时确定基础变量的类型是很难的事情(因为类型是C++在编译时期的游戏)。假设你有一个逻辑需求,需要动态使用入参的类型来做逻辑,怎么办呢?
比如:
template<typename T>
void foo(T arg)
{
if(T is 'float') ...
else ...
}
上面的需求是,如果 T 是 float 做一段编译逻辑,如果 T 不是 float 又做另一段编译逻辑。
也许你第一反应会用模板的各种特化来实现不同的 foo 的实现版本,这种做法没毛病。稍有缺点是不够直接体现在逻辑上。同时不够直接的缺点直接导致了很多模板化的后续操作无法依赖于此来进行(高阶操作详见后面)。
此时我们可以巧妙地用一个 struct 再配合特化来 wrap 一个bool来解决。
template<typename T>
struct Wrapper
{
static constexpr bool isFloat = false;
}
template<>
struct Wrapper<float>
{
static constexpr bool isFloat = true;
}
用法是这样的:
template<typename T>
void foo(T arg)
{
if(Wrapper<T>::isFloat) ...
else ...
}
总的来说上面的过程,利用模板的实例化和编译时的变量确定,实现了对类型进行判断,这个过程叫 Trait。
单词比较拗口,可以理解成一种对类型特质的获取。有人喜欢叫它“类型萃取”,name it whatever you want。
STD标准化
获取类型信息来做逻辑的需求,大部分已经在 std 库中标准化引入。
引入头文件:
#include <type_traits>
即可使用到如下几个内置模板等等:
std::cout << std::is_same<int, int>::value << std::endl; // 输出:true
std::cout << std::is_same<int, double>::value << std::endl; // 输出:false
std::cout << std::is_integral<int>::value << std::endl; // 输出:true
std::cout << std::is_integral<double>::value << std::endl; // 输出:false
std::cout << std::is_pointer<int*>::value << std::endl; // 输出:true
std::cout << std::is_pointer<int>::value << std::endl; // 输出:false
// ...
Enable_if 模板开关
enable_if 的实现非常简单,基本逻辑如下:
template<bool B, class T = void>
struct enable_if {};
template<class T>
struct enable_if<true, T> { typedef T type; };
代码上来看,enable_if 利用了偏特化来实现动态的类型定义。
这样做有什么用呢?说到 enable_if 就要先解释 SFINAE,Substitution Failure Is Not An Error,亦即(编译模板)替换的失败不看作是一种错误。
我们来看看 WIKI 上有关于 SFINAE 的代码:
struct Test {
typedef int foo;
};
template <typename T>
void f(typename T::foo) {} // Definition #1
template <typename T>
void f(T) {} // Definition #2
int main() {
f<Test>(10); // Call #1.
f<int>(10); // Call #2. Without error (even though there is no int::foo) thanks to SFINAE.
}
代码的注释已经非常清晰,即使 f(10) 匹配模板 void f(typename T::foo) 时会遇到 int::foo 不存在的问题,但编译器会忽略掉而不当作错误,继而继续寻找合适的匹配,也就是找到了正常的 void f(T),实例化为 void f(int) 。
这种行为在 C++98 时属于 UB(未定义行为),在 C++11 中标准化成为了 SFINAE。
SFINAE 让很多神奇的操作变为可能。我们用 enable_if 配合上类型提取,就可以实现超越重载的需求了,比如下面的示例(强烈建议跑起来观察观察):
#include <iostream>
#include <type_traits>
class foo;
class bar;
template<class T>
struct is_bar
{
template<class Q = T>
typename std::enable_if<std::is_same<Q, bar>::value, bool>::type check()
{
return true;
}
template<class Q = T>
typename std::enable_if<!std::is_same<Q, bar>::value, bool>::type check()
{
return false;
}
};
int main()
{
is_bar<foo> foo_is_bar;
is_bar<bar> bar_is_bar;
if (!foo_is_bar.check() && bar_is_bar.check())
std::cout << "It works!" << std::endl;
return 0;
}
std::enable_if<std::is_same<Q, bar>::value, bool> 使得在类型不匹配后,返回一个“不正确存在的类型”,跳过了模板匹配,从而获得正确的模板实例。
这种 “根据类型不同而实现不同逻辑” 的技巧,超越了重载决议的能力。不过C++工程未必要用的上这么拗口的需求,至于它的采用是不是必须的,就要看具体的工程要求了。