53 / 80 · C++17 · 约 9 分钟
Type traits 与 if constexpr 的编译期分支
Type traits 把类型性质表达为编译期值或类型变换;if constexpr 在模板实例化时丢弃不适用的分支。它解决的是不同类型需要不同合法表达式的问题,不会把运行期条件变成编译期事实。
性质查询与类型变换
C++11 的 <type_traits> 提供 is_integral、is_pointer 等查询,也提供 remove_reference、remove_cv 等变换。老形式通过 ::value 读取值、::type 读取类型;常见 _t 别名自 C++14 提供,标准库对应的 _v 简写从 C++17 普及,写例子时需要区分版本。
判断前先确定应不应该规范化类型。int、const int 与 int& 不是同一个类型;is_integral<int&> 为 false。泛型函数若关注被引用值的性质,可以先去引用再去顶层 const;若 const 本身影响可写性,就不能为了让 trait 返回 true 而盲目删除它。
丢弃分支不等于预处理删除
C++17 的 if constexpr 要求条件是常量表达式。在模板实例化中,条件确定后,不选中的分支不会被实例化。因此一个分支可以调用整型操作,另一个分支调用只有字符串才有的 size,而不要求两种类型同时满足两边接口。
但这不是 #if:编译器仍解析整个程序,非依赖名称的错误不能藏在死分支里;非模板代码里的错误类型表达式也不能靠 if constexpr(false) 随意豁免。要理解的是实例化选择,而不是“编译器完全不看这几行”。
让分支围绕明确支持的类型
示例用一个函数描述整数或 string,去除引用和顶层 const 后再查询类型。整数分支调用 to_string,字符串分支计算长度,其他类型通过依赖模板参数的 static_assert 给出清楚诊断。不同分支最终返回 string,调用者得到一致的结果类型。
traits 只回答它定义的具体问题。例如 is_move_constructible 说明可由某种右值构造,不必然说明存在专门的移动构造函数,更不证明移动很便宜。性能、语义与异常保证应分别检查;不要把一个名字像“支持移动”的 trait 当成所有优化的授权。
容易答错的地方
- 普通 if 即使条件编译期已知,也不会像模板里的 if constexpr 一样免去另一分支的实例化要求。
- 丢弃分支仍需合法语法;未声明的非依赖名称、错误头文件依赖等不能靠 if constexpr 隐藏。
运行一个例子
最低标准 C++17 · 完整程序 · 下载 .cpp
#include <cassert>
#include <iostream>
#include <string>
#include <type_traits>
template<class> constexpr bool supported = false;
template<class T>
std::string describe(const T& x) {
using U = std::remove_cv_t<std::remove_reference_t<T>>;
if constexpr (std::is_integral_v<U>) {
return "integer:" + std::to_string(x);
} else if constexpr (std::is_same_v<U, std::string>) {
return "text:" + std::to_string(x.size());
} else {
static_assert(supported<U>, "requires integer or string");
}
}
int main() {
const int n = 7;
std::string s = "abc";
assert(describe(n) == "integer:7");
assert(describe(s) == "text:3");
std::cout << describe(n) << ' ' << describe(s) << '\n';
}
在本地编译
g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread modern-traits.cpp -o example && ./example预期结果
integer:7 text:3
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
describe("abc") 为什么不进入 string 分支?怎样明确调用已有的字符串路径?
查看参考答案
字符串字面量是字符数组,通过 const T& 推导不会自动变成 std::string;去掉顶层限定后仍不是 string,所以落入不支持分支。写 describe(std::string("abc")) 可显式构造所需类型。若接口要零复制接收多种文本,应另行设计 string_view 入口,而不是误判数组类型。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。