我叫张三,是一名苦逼牛马程序员。
上一秒我还在工位上对着满屏的 typename std::enable_if_t<std::is_same_v<T, std::vector<std::map<...>::iterator>> 喷出一口老血,下一秒我就重生了。
好消息是,我没穿到异世界,也没穿到古代当王爷。
坏消息是,我穿进了一所幼儿园,名叫编译期幼儿园。
园长是个叫 Bjarne 的老头,笑呵呵地递给我一张胸牌,上面写着:"类型萃取小班"。
旁边一个小女孩跑过来,冲我喊道:"你连 T 是 const 还是 volatile 都分不清吗?老师说今天要考的!"
我低头一看,自己浑身光溜溜的,没有类型,没有名字,只有胸口印着一个大大的 auto。
那一瞬间我悟了,上辈子我被模板错误虐得死去活来,原来根源在这儿。
我连最基础的类型都没搞明白,就敢在泛型代码里裸奔,编译器看到后不给我写小作文才怪!
于是,我决定在幼儿园从头修炼 《类型学》 ,并把我的修炼笔记公开如下。
基础概念
咳咳,该正经的地方咱还是得正经点,不能丢份儿。
1. 类型萃取
类型萃取(Type Traits)嘛,就是可以在编译期获取和操作类型信息。
它不问"这个对象是什么值",而是问"这个类型本身有什么属性"。
比如经典的:
c++
std::is_pointer<int*>::value // true,是根指针,指哪儿打哪儿
std::is_pointer<int>::value // false,就是老实巴交的整数
std::is_pointer 本身就是一个萃取器,用于判断某个类型是否为指针类型。
它把 int* 扔进自己的检测仪,然后吐出 true(以静态常量 value 的形式)。
整个过程在编译期完成,一滴运行时开销都没有,一滴都没有哦。
2. 元函数
元函数听着怪吓人的,其实就是编译期函数。
普通函数:我们给它 (int a, int b),它运行时算出 a+b 返回。
元函数:我们给它 <typename T, typename U>,它编译时 算出 constexpr bool 或者一个新的类型返回。
模板就是元函数的语法载体。比如:
c++
template<typename T>
struct add_const
{
using type = const T; // 返回一个加过const的新类型
};
这里 add_const 是个元函数,输入 int,输出 const int(通过 add_const<int>::type 拿结果)。
当然,标准库也预装一套常用类型萃取元函数,省得我们自己手搓这些轮子。
3. 模板特化
如果元函数是函数,那它总得有条件分支吧?
但编译期没有 if 语句,这时候模板特化就扛着品如的衣柜进来啦。
我们来自己实现一个std::is_pointer(简化版):
c++
// 主模板:默认情况,不是指针
template<typename T>
struct is_pointer
{
static constexpr bool value = false;
};
// 偏特化:匹配 T* 的情况
template<typename T>
struct is_pointer<T*>
{
static constexpr bool value = true;
};
编译器遇到 is_pointer<int*> 时,先看主模板:哦,你要一个 T。
然后发现有个特化版本 is_pointer<T*>,并且 int* 完美契合 T* 的模式。
啪的一下!很快奥,上来就是一个左正蹬,一个右鞭腿,一个左刺拳。
嘿,您猜怎么着?直接匹配成功,选择特化版,value 变 true。
遇到 is_pointer<int>,模式 T* 不匹配,退回主模板,value=false。
这就是编译期的模式匹配。
模板特化是元编程的唯一控制流机制,没有它,元函数只能返回一样的值,跟复读机似的。
4. 为什么叫"萃取"不叫"检测"?
因为萃取经常不只是返回个 bool,还能把类型的零件拆出来。
c++
std::remove_pointer<int*>::type // 得到 int
std::add_pointer<int>::type // 得到 int*
假如我们有个橘子,我们想让萃取器帮我们检查这正不正宗,味儿足不足。
它不仅能告诉我们这橘子可真是个橘子,还能顺带帮我们把皮剥了。
甚至还能去籽,然后榨汁一条龙服务。
5. 总结一下
- 元函数:编译期干活的函数,输入类型/常量,输出类型/常量。
- 类型萃取:标准库提供的一堆实用元函数,专门榨取类型信息。
- 模板特化:元函数写 if-else 的语法,没有它,元函数就是个残废。
标准库类型萃取概览
我们对 <type_traits> 里的东西按用途进行分类。
这些东西随便扫几眼就行,需要的时候再查。
1. 第一类:类型属性查询
这帮萃取器专门回答:"这类型是干嘛的?你是什么成分?你的祖上又是什么成分?"
c++
std::is_void<T> // 是不是void?空无一物?
std::is_integral<T> // 是不是整数?(bool/char/int/long都算)
std::is_floating_point<T> // 是不是浮点?float/double/long double
std::is_array<T> // 是不是数组?int[5]算,int*不算!
std::is_enum<T> // 是不是枚举?编译器知道,我们未必看得出
std::is_union<T> // 是不是联合体?上古遗产检测仪
std::is_class<T> // 是不是class/struct?注意:int不算,std::string算
std::is_function<T> // 是不是函数类型?int(int, char)这种,不是函数指针
std::is_pointer<T> // 是不是原生指针?
std::is_lvalue_reference<T> // 是不是左值引用&?
std::is_rvalue_reference<T> // 是不是右值引用&&?
std::is_member_pointer<T> // 是不是成员指针?int MyClass::*
还有修饰符检测系列:
c++
std::is_const<T> // 是不是const?
std::is_volatile<T> // 是不是volatile?(见过吗?我也没怎么见过)
std::is_trivial<T> // 是不是平凡类型?能像C结构体那样memcpy?
std::is_trivially_copyable<T> // 同上,但允许有非平凡构造,只要拷贝安全
std::is_standard_layout<T> // 是不是标准布局?内存排布和C兼容?
std::is_pod<T> // C++17后已弃用,老代码里常见,等于trivial+standard_layout
std::is_abstract<T> // 有没有纯虚函数?不能实例化
std::is_final<T> // C++14,是不是final类?
std::is_empty<T> // 是不是空类?无成员变量,sizeof可能为1
我们把身份证给编译器检查,编译器扫了一眼:"良民,大大滴良民"。
2. 第二类:类型转换
这帮萃取器能把一种类型变成另一种,削掉引用、剥掉const、加个指针等。
c++
// 去修饰符
std::remove_const<T> // 把const T变成T
std::remove_volatile<T> // 同上
std::remove_cv<T> // 一起剥,const volatile全干掉
std::remove_reference<T> // T&或T&&变成T
std::remove_pointer<T> // T*变成T
std::remove_extent<T> // int[5]变成int,只去掉第一层[]
std::remove_all_extents<T> // int[3][4]变成int
// 加修饰符
std::add_const<T> // T变成const T
std::add_volatile<T> // ...
std::add_cv<T> // 双重加料
std::add_lvalue_reference<T> // T变成T&
std::add_rvalue_reference<T> // T变成T&&
std::add_pointer<T> // T变成T*
// 特殊变形
std::decay<T> // 值传递时的退化:数组变指针,函数变指针,去掉引用和顶层const
std::make_signed<T> // int变int,unsigned变int,char变signed char
std::make_unsigned<T> // 反过来
std::enable_if<B, T> // B为true时才有type=T,否则type缺失
std::conditional<B, T, F> // B为true得T,false得F
std::common_type<Ts...> // 一堆类型里选一个大家都能隐式转换到的公共类型
std::underlying_type<T> // 枚举的底层整数类型
std::result_of<F(Args...)> // C++17已弃用,用invoke_result替代,函数调用返回类型
3. 第三类:类型关系
判断两个类型之间能不能赋值?继承?构造?
c++
std::is_same<T, U> // 是不是完全一样?包括const/volatile修饰
std::is_base_of<Base, Derived> // Base是不是Derived的基类?公开/保护/私有继承都算
std::is_convertible<From, To> // From能不能隐式转换成To?
std::is_constructible<T, Args...> // 能不能用Args...构造T?
std::is_trivially_constructible<T, Args...> // 平凡构造
std::is_nothrow_constructible<T, Args...> // 构造不抛异常
// 类似还有 is_assignable, is_copy_constructible, is_move_constructible 等
举个栗子:
c++
template<typename T>
std::enable_if_t<std::is_base_of<Animal, T>::value, void> feed(T& animal)
{
animal.eat();
}
// 只有Animal的子类才能喂食
4. 辅助别名_t和变量模板_v(C++ 14)
4.1 变量模板:_v
以前写:
c++
if (std::is_pointer<T>::value) { ... }
现在写:
c++
if constexpr (std::is_pointer_v<T>) { ... }
_v 后缀直接把 ::value 给我们取出来了。
4.2 别名模板:_t
以前写:
c++
typename std::remove_reference<T>::type elem; // 最前面的typename能消歧义
现在写:
c++
std::remove_reference_t<T> elem; // 干净利落
实现它们也很简单:
c++
template<typename T>
using remove_reference_t = typename remove_reference<T>::type;
template<typename T>
inline constexpr bool is_pointer_v = is_pointer<T>::value;
所以它们只是用语法糖包装了一下。
一些常见类型萃取的实现原理
我们看几个经典的萃取期源码,当然还是简化版的。
1. std::is_same
功能:判断两个类型是不是完全一样,连const、引用这种细节都对得上。
实现:
c++
template<typename T, typename U>
struct is_same : std::false_type {}; // 默认:不相同
template<typename T>
struct is_same<T, T> : std::true_type {}; // 特化:相同
这里 std::true_type 和 std::false_type 是啥?其实就是:
c++
using true_type = std::integral_constant<bool, true>;
using false_type = std::integral_constant<bool, false>;
而 integral_constant<T, v> 是一个包装了编译期常量的类模板,提供 ::value 静态成员和类型别名 ::type。
它主要的作用是在编译期存储和表示一个常量值。
其值是存储在 ::value 静态成员中的。
小芝士介绍完了,我们来看看原理:
- 主模板接收两个独立类型参数T和U,继承false_type(即value=false)。
- 偏特化版本:is_same<T, T>,当第二个参数和第一个参数字面相同时,编译器会选中这个更匹配的偏特化,继承true_type(value=true)。
用法:
c++
static_assert(is_same<int, int>::value); // true
static_assert(!is_same<int, const int>::value); // false
(其实 is_same 实现是从我之前的一篇文章中直接复制过来的,这就叫代码复用,嘿嘿)
2. std::remove_const
功能:把const T变成T,如果不是const就原样返回。
实现:
c++
template<typename T>
struct remove_const
{
using type = T; // 默认:啥也不干
};
template<typename T>
struct remove_const<const T>
{
using type = T; // 特化:匹配到const T,把const剥了
};
这个 remove_const 只剥顶层 const。
比如 const int* 的 const 修饰的是 int,不是指针本身,所以 const int* 匹配不到 const T。
3. std::is_pointer
功能 :检查类型是不是原生指针(包括int 、void、const int*等)。
我们之前实现过,这次再精确点:
c++
template<typename T>
struct is_pointer : std::false_type {};
template<typename T>
struct is_pointer<T*> : std::true_type {}; // 捕获普通指针
template<typename T>
struct is_pointer<T* const> : std::true_type {}; // 捕获const指针
template<typename T>
struct is_pointer<T* volatile> : std::true_type {}; // 捕获volatile指针
template<typename T>
struct is_pointer<T* const volatile> : std::true_type {};
我们用四个偏特化覆盖所有指针的cv限定组合,其余全部掉入主模板返回false。
真实标准库还会多考虑一些,但思路就这样。
4. std::conditional
功能:if (B) then T else F,但发生在编译期。
实现:
c++
template<bool B, typename T, typename F>
struct conditional
{
using type = F; // 默认:false分支
};
template<typename T, typename F>
struct conditional<true, T, F>
{
using type = T; // 特化:true分支
};
测试(使用我们之前实现的 is_same):
c++
is_same<conditional<true, int, float>::type, int>::value; // true
is_same<conditional<false, int, float>::type, float>::value; // true
应用:比如写一个 std::make_unsigned,如果 T 是 int 得到 unsigned int,否则保持原样(简化版)。
c++
template<typename T>
struct make_unsigned
{
using type = std::conditional_t<
std::is_same_v<T, int>,
unsigned int,
T
>;
};
5. std::void_t(C++ 17)
这是最有趣的一个,它本身是个空壳子,但配合 SFINAE 能检测任意表达式是否合法。
定义:
c++
template<typename...>
using void_t = void; // 无论给什么参数,我都生成void
利用 SFINAE 规则"模板参数替换失败不是错误"。
当我们在一个偏特化里写 void_t< decltype(表达式) > 时,如果 decltype(表达式) 不合法,整个偏特化就被静默丢弃,编译器退回到主模板。
经典案例:检测一个类型是否有.size()成员函数。
c++
// 主模板:默认false
template<typename, typename = void>
struct has_size : std::false_type {};
// 偏特化:如果T有.size()成员,偏特化被选中
template<typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
: std::true_type {};
测试:
c++
has_size<std::vector<int>>::value; // true
has_size<int>::value; // false
std::declval<T>():一个编译期工具,假装创建一个 T 的右值引用,让我们能在不求值上下文里调用成员函数,获得返回类型。
它本身没有定义,只能在decltype里用。
有点偏题了,讲到 SFINAE 去了(好像也没有偏题?)。
6. 再啰嗦几句
标准库里那些看起来很厉害的萃取器,剥开来基本都是这几个套路的排列组合。
现在我们自己去看那些萃取器的源码,也能看懂个七七八八的。
ok,就先拆解到这了,接下来就开始我们的手搓环节。
自定义类型萃取
标准库里的萃取器虽然多种多样,但总有它触及不到的地方,这时候就需要我们亲自动手。
1. 什么时候该自己动手?
别急着撸袖子。开始之前先想想这些问题:
- 标准库有吗?能偷懒就偷懒嘛,没有再搓轮子。
- 概念(Concepts,C++20)能更干净地解决吗?如果我们能用 std::integral 直接约束,就别写一坨enable_if。
- 我们真的需要在编译期分支吗?如果只是重载决议,有时候一个普通的函数重载比萃取更直观。
如果说:"不,我就要在模板里根据某个类的特定成员决定走哪条路"。
那自定义萃取就是我们的好朋友。
2. 核心工具
无论检测什么,都离不开这三位大爷:
- std::void_t<...>:SFINAE的开关,C++17起标准提供,但自己写一行也行。
- std::declval<T>():假装创建一个T的实例,让我们能在decltype里调用它的成员或运算符,永远不能出现在求值上下文里。
- decltype(...):捕获表达式的类型。
把它们组合起来,模式如下:
c++
// 默认返回false的主模板
template<typename, typename = void>
struct MyTrait : std::false_type {};
// 偏特化:如果某个表达式合法,void_t成功变成void,匹配此特化,返回true
template<typename T>
struct MyTrait<T, std::void_t<decltype( /* 我们的表达式 */ )>>
: std::true_type {};
这就是自定义萃取的万能模板,后面咱们所有的花样都是往 decltype 里填不同的东西。
3. 检测成员类型
比如我们想知道一个类内部有没有定义value_type。
c++
template<typename, typename = void>
struct has_value_type : std::false_type {};
template<typename T>
struct has_value_type<T, std::void_t<typename T::value_type>>
: std::true_type {};
测试:
c++
static_assert(has_value_type<std::vector<int>>::value); // true
static_assert(!has_value_type<int>::value); // false
typename T::value_type 如果存在,它就是合法类型,void_t 得到 void;如果不存在,模板替换失败,偏特化被丢弃,落回主模板。
辅助别名(方便直接用):
c++
template<typename T>
inline constexpr bool has_value_type_v = has_value_type<T>::value;
4. 检测成员函数
这是最常见的需求。
我们之前也实现过一个检测有没有无参的.size()方法。
这次就来实现一个检测有没有 void clear(int):
c++
template<typename T, typename = void>
struct has_clear_int : std::false_type {};
template<typename T>
struct has_clear_int<T,
std::void_t<decltype(std::declval<T>().clear(std::declval<int>()))>
> : std::true_type {};
测试:
c++
struct A { void clear(int); };
struct B { void clear(double); };
struct C { int clear; }; // 成员变量不是函数
static_assert(has_clear_int<A>::value); // true
static_assert(!has_clear_int<B>::value); // false,参数类型不匹配
static_assert(!has_clear_int<C>::value); // false
5. 检测操作符
检测一个类型是否支持 + 运算符(两个const T&相加):
c++
template<typename T, typename = void>
struct is_addable : std::false_type {};
template<typename T>
struct is_addable<T,
std::void_t<decltype(std::declval<const T&>() + std::declval<const T&>())>
> : std::true_type {};
通用版:检测任意二元运算符(比如 << 流输出):
c++
template<typename T, typename Stream = std::ostream, typename = void>
struct is_streamable : std::false_type {};
template<typename T, typename Stream>
struct is_streamable<T, Stream,
std::void_t<decltype(std::declval<Stream&>() << std::declval<T>())>
> : std::true_type {};
6. 复合条件
有时候我们需要一个萃取回答"这个类既有begin()又有end(),而且它们的返回类型能比较"。
这就得组合多个表达式。
方法一:void_t里塞多个decltype
c++
template<typename T, typename = void>
struct is_range : std::false_type {};
template<typename T>
struct is_range<T, std::void_t<
decltype(std::declval<T>().begin()),
decltype(std::declval<T>().end()),
// 可以加更多要求,比如 begin() 返回的迭代器支持 != 比较
decltype(std::declval<T>().begin() != std::declval<T>().end())
>> : std::true_type {};
void_t 是个变参模板,它会把所有 decltype 都求值一遍(实际是检查合法性)。
只要有一个非法,整个替换就失败,偏特化丢弃。
方法二:逻辑串联,用 std::conjunction
先来简单介绍一下 std::conjunction:
它是 C++17 引入的模板工具,能将一组类型按顺序组合成一个新的类型。
只有当所有类型的 value 都为 true 时,其结果才为 true。
比如 std::conjunction<A, B, C>,只有当 A,B,C 萃取出的 value 都为 true 时,结果才为 true。
它与 && 一样,也有短路行为,一旦值可以确定就会停止。
也就是说当 A 为 false 时,就会直接停止。
我们来使用一下:
c++
template<typename T>
using is_vector_like = std::conjunction<
has_size<T>, // 有 .size()
has_value_type<T>, // 有 value_type 嵌套类型
std::is_same<typename T::value_type,
std::decay_t<decltype(std::declval<T>()[0])>> // 下标返回类型与value_type一致
>;
这样is_vector_like::value就是三个条件的逻辑与。
7. 最后的最后
- 我们可以与 C++14 的 _v 和 _t 别名一样,自己写的萃取也可以模仿。
- 如果我们发现自己在写一个检测"有 getFoo 且返回 const Bar* 且 Bar 有嵌套 Baz......"的五层萃取,可能是设计上该用虚函数 或概念重构了。
- 当我们的萃取意外失败时,错误信息会是一大坨。这时候我们可以先从 decltype 里的表达式一行行注释掉,看哪一步炸的。
结尾
(差不多该收工了)
所以我们看,类型萃取这玩意儿,说穿了就是编译期的摸骨算命。
我们写个模板,它不放心,非得把传进来的类型扒光了瞅瞅:"嗯,是个指针,骨重六两;带 const,命里带锁;有个叫 size() 的成员,能吃容器这碗饭。"
学这东西最大的好处,不是写库,也不是炫技。
是以后看源码时,不再觉得自己在看天书。