ARTICLE · 1280653
让模板学会"看人下菜碟": 类型萃取与编译期技巧
让模板学会"看人下菜碟": 类型萃取与编译期技巧学完第 13 篇的模板入门, 你已经能写出"对任何类型一视同仁"的泛型函数. 但现实世界往往反过来: 对整型要按位运算, 对浮点要按数学运算, 对字符串要有另一套处理. 于是你开始撞见 std::is_same, std::enable_if, decltype 这些词, 每个都眼熟, 连起来却一头雾水. 本篇把它们串成一条线, 讲清楚四件事: 怎么在编译期向类型提问, 怎么在编译期分流, 怎么不创建对象就推导出类型, 怎么处理任意数量的参数. 全部基于 C++17, 语法拿来就能用, 不需要任何外部库. 先建立一个直觉: 在模板的世界里, "判断"和"分支"都发生在编译期, 运行期代码里根本看不到这些痕迹. 带着这个直觉, 下面四节会顺畅很多. 这套技巧最典型的用途, 就是写容器包装类, 序列化库这类"要兼容各种类型"的公共代码. type_traits 头文件里有一族"问题函数", 名字都是 is_xxx. 运行期它们什么都不做, 编译期却能告诉你铁一般的事实. C++17 之后, 每个都附赠了 _v 后缀的变量模板版本, 写起来干净利落: static_assert 是编译期就拍板的检查: 条件不满足, 编译直接失败, 这就是"在编译期向类型提问"的价值: 把运行时才暴露的 bug, 提前到编译期拦下. 除了 is_integral, is_same, 还有 is_pointer, is_class, is_convertible, is_trivially_copyable 等几十个, 组合起来几乎能描述任何你想知道的类型特征. 当你写的模板需要"查类型户口"时, 第一反应就应该是这里. 比如判断两个类型是否有继承关系可以用 std::is_base_of, 判断能否构造出对象可以用 std::is_constructible, 写框架代码时经常用得上. 这些 trait 在内部就是编译期常量, 所以它们能出现在 static_assert 里, 能当模板参数, 甚至能决定一个数组的大小. 在类型面前, 它们比任何运行时的 if 都可靠: 没有运行, 就没有运行时出错. 知道类型之后, 下一步是"按类型走不同的代码". C++11 时代的标准答案是 std::enable_if: 把条件塞进函数模板的返回类型或参数列表, 条件不满足就把这个重载从候选集里移除. 能工作, 但可读性一言难尽, 报错更是天书, 新手根本不敢碰. C++17 给了现代答案: if constexpr. 规则一句话: 条件在编译期求值, 只编译满足条件的那一个分支, 另一个分支连语法合法性都不检查: if constexpr 不是"快一点的 if", 而是"编译期的分支语句": 四个分支的代码可以互相矛盾而不报错, 因为编译器只保留了一条. 在函数模板里, 你就能用"普通思维"写代码, 再也不用堆模板体操. 可以说, 它取代了 90% 的 enable_if 场景, 如果你在旧代码里看到 enable_if, 先想想能不能换成 if constexpr. 小提醒: if constexpr 的条件必须能在编译期求值, 所以它的黄金搭档正是第 1 节的 is_xxx 族, 或者你自写的 constexpr 函数 - 第 9 篇学到的 constexpr, 在这里派上了大用场. decltype(表达式) 的意思是"这个表达式的类型", 它不求值表达式, 只问类型. 最经典的用法是推导模板函数的返回类型, 让函数能处理"任何可相加的东西": declval() 更妙: 它"虚构"一个 T 类型的右值引用, 但并不会真正构造任何对象. 你可以在只有类型, 没有值的情况下, 问"这个类型能做什么运算", 配合 decltype, sizeof, is_xxx 使用. 一句话: declval 是类型世界的"纸片人", 只在编译期的推理舞台上出现, 永远不会有真实本体. 再给一个高频组合: decltype(std::declval().size()) 不求值任何对象, 就能问出"类型 A 的 size() 返回什么", 这是判断"一个类型支持哪些操作"的标准姿势, 也是将来写高级约束的地基. 函数想接收"任意数量的参数", 靠重载写不过来, 靠数组又类型单一. 可变参数模板的答案是参数包: 用 ...Args 接住一箩筐东西, 再用折叠表达式一次性展开: 折叠表达式 (args + ...) 把参数包从左到右展开, 二元操作符可以换成任何东西, 包括输出流 <<, 于是 printAll 三行搞定"打印任意数量参数". std::tuple 则是"异构容器": 一个 tuple 里可以同时装 int, double, string, 用 std::get 按位置取; C++17 的 std::apply 还能把 tuple 展开后当参数传给函数. 两者合起来, 就是"把不定数量, 不定类型的东西收进一个盒子", 这是无数高级库的地基, 从今天起你终于能读懂它们了. 折叠表达式还有一元形态, 比如 (... && args) 把参数包按逻辑与展开, "判断所有参数都为正"可以一行写完, 留给你当课后作业. 另外, std::apply 和 std::make_from_tuple 是 tuple 的黄金搭档: 前者把 tuple 拆开当参数, 后者用 tuple 直接构造对象, 强烈建议查手册自己跑一遍, 跑通你就彻底掌握 tuple 了. 回头看这条线: 类型萃取在编译期"认识"类型, if constexpr 在编译期"分流", decltype 和 declval 在编译期"推导类型", 折叠表达式与 tuple 在编译期"收纳任意参数". 四件套合起来, 你就有了把"运行时的多态"换成"编译期的决策"的能力: 同样的功能, 没有虚函数开销, 错误还提前到编译期暴露. 这, 就是模板从入门走向进阶的分水岭, 也是你在第 9 篇学到的 constexpr 精神的延伸: 能编译期做的, 绝不留到运行期. 觉得醍醐灌顶就点个"在看". 下一篇我们把镜头转向代码风格: RAII, Rule of Zero, pimpl, enum class, 看看专业项目的代码是怎么写的. 关注本号, 30 篇持续更新.
1. 类型萃取: 在编译期向类型提问
#include<iostream>#include<type_traits>template <typename T>voidshowType(){std::cout << "integral: " << std::is_integral_v<T> << '\n'<< "floating: " << std::is_floating_point_v<T> << '\n'<< "same as int:" << std::is_same_v<T, int> << '\n';}intmain(){static_assert(std::is_integral_v<int>);static_assert(!std::is_integral_v<double>);static_assert(std::is_same_v<int, int>);showType<int>(); // integral: 1, floating: 0, same as int: 1showType<double>(); // integral: 0, floating: 1, same as int: 0return 0;}
2. if constexpr: 让模板学会"分流"
#include<iostream>#include<string>#include<type_traits>template <typename T>std::string typeName(){ifconstexpr(std::is_integral_v<T>){return "integral";} else if constexpr (std::is_floating_point_v<T>) {return "floating point";} else {return "something else";}}intmain(){std::cout << typeName<int>() << '\n'; // integralstd::cout << typeName<double>() << '\n'; // floating pointstd::cout << typeName<std::string>() << '\n'; // something elsereturn 0;}
3. decltype 与 declval: 不建对象, 也知道类型
#include<iostream>#include<string>#include<type_traits>#include<utility>template <typename T, typename U>auto add(T a, U b) -> decltype(a + b) { // 返回类型: a + b 的类型return a + b;}int main() {auto r = add(1, 2.5); // int + double -> doublestatic_assert(std::is_same_v<decltype(r), double>);// declval: 不真正构造对象, 也能问"它是什么类型"using Size = decltype(sizeof(std::declval<std::string>()));static_assert(std::is_same_v<Size, std::size_t>);return 0;}
4. 可变参数模板: 一锅端, 以及收纳盒 tuple
#include<iostream>#include<string>#include<tuple>template <typename... Args>autosumAll(Args... args){return (args + ...); // 折叠: 展开成 a + b + c + d}template <typename... Args>voidprintAll(const Args&... args){(std::cout << ... << args); // 折叠输出流}intmain(){std::cout << sumAll(1, 2, 3, 4) << '\n'; // 10printAll("sum = ", 1 + 2 + 3, '\n'); // sum = 6// tuple: 异构的收纳盒, 一个盒子装下不同类型std::tuple<int, double, std::string> t{1, 2.5, "hello"};std::cout << std::get<0>(t) << ' '<< std::get<1>(t) << ' '<< std::get<2>(t) << '\n';return 0;}















































































