CppCon 2014 学习:Modern Template Metaprogramming A Compendium
理解说明:
“Modern Template Metaprogramming: A Compendium” 这个标题通常指的是一组 现代 C++ 模板元编程(Template Metaprogramming,TMP) 技术、工具和模式的合集。
什么是 Template Metaprogramming?
模板元编程是使用 C++ 的模板系统,在编译期执行逻辑、计算类型、生成代码结构的一种编程方式。它常用于:
- 编译期类型检查
- 条件编译逻辑
- 代码生成与优化
- 泛型算法
- 静态反射
“Modern” 表示什么?
现代 TMP 与早期 TMP(例如 C++98/03 中的 boost::mpl
)相比,有许多改进:
旧 TMP(C++98) | 现代 TMP(C++11/14/17/20) |
---|---|
boost::mpl 、手动递归 | constexpr , if constexpr |
手写元函数结构体 | template <typename...> 和 auto 泛型 lambda |
手动递归列表操作 | 使用 std::tuple , std::integer_sequence |
没有反射 | 结构化绑定、concepts 、模板约束 |
模板特化判断类型 | std::is_same , std::enable_if , std::conditional_t 等工具辅助 |
A Compendium:包含什么?
通常这样的“汇编”会介绍这些现代 TMP 工具和技巧:
类型工具(Type Traits)
std::is_same
,std::is_base_of
,std::is_integral
, …std::enable_if
,std::conditional
,std::decay
,std::remove_cvref
…
constexpr + 模板逻辑
constexpr if
- 编译期数组、查找表、递归
变参模板(Variadic Templates)
- 操作类型包(
template <typename... Ts>
) - 展开参数包
- 元编程算法(例如 filter/map/fold)
类型列表(Typelist)
- 如
type_list<Ts...>
- 与 fold/map/reduce 等组合
SFINAE & Concepts
- 替代传统 SFINAE:
requires
,concept
,constrain auto
- 更好的错误信息与限制机制
模板 Lambda 与函数对象
auto
lambda 参数(C++14 开始)template<class T> constexpr auto F = [](T x){ ... };
静态反射(C++20/23 提案)
- 反射类型名、字段、方法等结构(目前通过宏/工具模拟)
示例:变参计数器
template<typename... Ts>
constexpr std::size_t count = sizeof...(Ts);
static_assert(count<int, double, char> == 3);
示例:类型筛选器
template<typename... Ts>
using only_integrals = std::tuple<std::conditional_t<std::is_integral_v<Ts>, Ts, void>...>;
目标用途
现代 TMP 的应用领域包括但不限于:
- 静态数据建模
- 编译期 JSON / 配置解析
- 高性能泛型库(如 ranges、fmt、protobuf)
- 类型安全系统(serialization, variant, visitor)
总结
“Modern Template Metaprogramming: A Compendium” 是对 C++20/23 时代模板元编程技术的系统梳理,强调:
- 简洁、类型安全
- 编译期逻辑表达能力增强
- 使用标准库 trait 和 constexpr 工具替代手写元结构
- 可读性和可维护性显著提高
Abstract 解读(摘要)
这是一次关于 现代 C++ 模板元编程(Template Metaprogramming) 的技术讲座摘要,适合已经具备一定 C++ 基础的人群,特别是模板编程方面有经验的开发者。
讲座目的
“Template metaprogramming has become an important part of a C++ programmer’s toolkit.”
- 模板元编程已经是现代 C++ 工程中不可或缺的一部分。
- 本次演讲将:
- 展示 最先进的 TMP 工具和技巧
- 利用这些技术,重新实现标准库中一些常见功能(如
tuple
,variant
,optional
等)
技术亮点
“void_t, a recently-proposed, extremely simple new <type_traits> candidate…”
- 特别提到了
void_t
:这是一个极简的、但功能非常强大的type_traits
工具,用于实现 SFINAE 检测(Substitution Failure Is Not An Error)——在模板中判断某个表达式是否合法。 void_t
的用法虽然简单,却被认为是“高级且优雅的”,甚至能让一些有经验的模板开发者感到惊喜。
template<typename, typename = void>
struct has_type_member : std::false_type {};
template<typename T>
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type {};
- 上面这段就是利用
void_t
实现的经典 检测某类型是否有嵌套type
成员 的技巧。
演讲结构
- 分为 两部分,中间有一个简短的休息。
- 内容涉及高阶 TMP 概念,不适合初学者。
- 明确提示:不是为 C++ 新手准备的!
- 如果你是初学者,这次可能“跳级”了,但未来可以再来。
总结
项目 | 内容 |
---|---|
目标 | 展示现代 TMP 技术,尤其是可用于实现标准库机制的技术 |
特点 | 包括 void_t 等精简而强大的技巧 |
注意 | 高阶内容,不适合新手 C++ 开发者 |
如果你希望我帮你 解释 void_t 的底层原理、用例场景,或者希望我复刻一个标准库功能(比如用 TMP 实现一个简化版的 optional
或 variant
)
关于讲者本人(A little about me)
这段内容是讲者的自我介绍,展示了其专业背景、行业经验以及在 C++ 领域的深厚积累。
教育背景
- 本科:数学(B.A. in Mathematics)
- 硕士 & 博士:计算机科学(M.S., Ph.D. in Computer Science)
专业经历
- 从事专业编程工作接近 50 年
- 自 1982 年起使用 C++ 编程(C++ 发布初期)
- 在多个领域拥有实际经验:
- 工业界
- 学术界
- 顾问咨询
- 科研机构
教育与领导经验
- 创立了一个计算机系(Founded a CS Dept.)
- 曾担任教授和系主任
- 教授并指导各层次学生
管理与培训经验
- 管理并指导销售渠道的编程团队
- 作为软件顾问和商业培训师在全球讲课
- 曾在**Fermilab(费米国家实验室)**科学计算部门任职,专注 C++ 和内部咨询
当前状态
- 已退休(from Fermilab)
- 但仍然 可以提供咨询服务(Not dead!)
总结
内容 | 说明 |
---|---|
学术背景 | 数学本科 + 计算机科学硕博 |
编程经验 | 近 50 年,C++ 使用超过 40 年 |
多重角色 | 教授、主管、顾问、讲师、开发者 |
机构背景 | Fermilab 科学计算部门 |
当前状态 | 退休但仍可接项目或顾问工作 |
讲者在 C++ 标准化领域的地位与贡献
1. 资深(Emeritus)C++ 标准化参与者
- 曾是 C++ 标准委员会 WG21 的活跃成员
- 现在是 Emeritus(荣誉)参与者,意味着已不再担任正式委员,但其贡献和地位仍被认可
2. 撰写过 85+ 标准提案论文
- 这些论文为 C++ 标准引入了多项重要库特性,比如:
cbegin
/cend
:用于获取容器的常量迭代器common_type
:用于推导多个类型的共同类型- 以及完整的
<random>
和<ratio>
头文件实现
3. 深刻影响了核心语言特性
- 贡献包括但不限于:
- 别名模板(alias templates)
- 上下文转换(contextual conversions)
- 变量模板(variable templates)
4. 重要标准项目编辑角色
- 担任过 ISO/IEC 29124 国际标准(数学特殊函数的 C++ 标准)的项目编辑
- 现为 C++17 标准的副项目编辑(Associate Project Editor)
5. 强烈且专业的观点
- 基于丰富的培训和实践经验,讲者持有一些较为强烈的观点
- 这些观点不一定被所有程序员认同,但讲者认为它们应该被接受和采纳
总结
重点 | 说明 |
---|---|
标准委员会资深成员 | 参与 WG21,撰写了 85+ 提案 |
库功能贡献 | cbegin/cend , common_type , <random> , <ratio> |
语言特性贡献 | 别名模板、上下文转换、变量模板 |
标准项目编辑 | ISO 29124 项目编辑,C++17 副项目编辑 |
专业观点 | 有独特且坚定的编程方法论观点 |
预览的示例内容(无特定顺序)
1. 来自标准库 (std::
) 的典型元编程工具
-
类型常量和类型判断
integral_constant
:封装常量值的类型true_type
,false_type
:分别表示布尔真值和假值的类型is_same
:判断两个类型是否相同is_void
:判断类型是否为void
is_integral
:判断是否为整数类型is_floating_point
:判断是否为浮点数类型is_signed
:判断是否为有符号类型
-
赋值能力检查
is_copy_assignable
:类型是否可拷贝赋值is_move_assignable
:类型是否可移动赋值
-
类型修饰符去除
remove_const
:移除const
remove_volatile
:移除volatile
remove_cv
:移除const
和volatile
-
条件类型和启用技巧
conditional
:基于条件选择类型enable_if
:SFINAE 技巧,启用或禁用模板实例化
-
迭代距离
distance
:计算迭代器范围长度
2. 尚未进入标准库(或不一定有)的工具
-
数学相关
abs
:绝对值(常见但未必在元编程中)gcd
:最大公约数
-
自定义类型检查和常量
type_is
:判断类型类别(自定义)bool_constant
:类似于integral_constant<bool, ...>
is_one_of
:判断一个类型是否属于给定类型列表void_t
(重要且高级的工具):用于检测模板中的合法类型成员has_type_member
:检查类型是否包含特定成员类型is_valid
:SFINAE 友好的检查模板表达式是否合法is_complete
:检查类型是否已完全定义(非不完整类型)
总结
类别 | 示例工具 | 功能简介 |
---|---|---|
标准库元编程工具 | integral_constant , is_same , enable_if 等 | 类型判断、条件选择、SFINAE等 |
非标准(或自定义) | void_t , is_one_of , has_type_member 等 | 高级模板检测、类型列表判断 |
什么是模板元编程(Template Metaprogramming)?
定义(来源 Wikipedia)
- **元编程(Metaprogramming)**是编写计算机程序的一种方式,这些程序:
- 写出或操作其他程序(或者它们自身)作为数据,或者
- 在编译期执行某些工作,而不是在运行时执行
C++ 中的模板元编程
- C++ 模板元编程利用 模板实例化 机制,在编译时完成计算和逻辑推断。
- 当代码中出现一个模板名字,且期待是函数、类型或变量时,编译器会根据模板参数生成(实例化)对应的实体。
举例
- 函数模板调用:
f(x)
,其中f
是一个函数模板,编译器会在编译时根据x
的类型实例化f
。
优点
模板元编程的目的是为了:
- 提高源码的灵活性 — 让代码更通用,更容易适应不同类型和需求。
- 提高运行时性能 — 把一些本应运行时执行的计算提前到编译期,减少运行时开销。
这是2001年针对 std::pow()
函数和两个模板版本 pow<> v1
、pow<> v2
的性能测试结果:
函数 | real 时间 | user 时间 | sys 时间 |
---|---|---|---|
std::pow() | 11.858 秒 | 11.837 秒 | 0.020 秒 |
pow<> v1 | 8.081 秒 | 8.081 秒 | 0.020 秒 |
pow<> v2 | 3.035 秒 | 3.024 秒 | 0.030 秒 |
备注:
- 测试在 700MHz PIII CPU、Windows 2000 上进行。
- 使用 gcc 2.95.2 编译器,且关闭了优化(
-O0
)。 - 每个测试运行 10,000,000 次,并且重复 50 轮。
结论:
- 模板版本
pow<>
显著快于标准库的std::pow()
。 pow<> v2
性能最佳,显示了模板元编程通过在编译期计算,可以极大提升运行时效率。
这是2001年针对启用优化编译(g++ -O2
)后,std::pow()
与两个模板版本 pow<> v1
、pow<> v2
的性能测试结果:
函数 | real 时间 | user 时间 | sys 时间 |
---|---|---|---|
std::pow() | 11.857 秒 | 11.847 秒 | 0.020 秒 |
pow<> v1 | 4.286 秒 | 4.236 秒 | 0.010 秒 |
pow<> v2 | 0.300 秒 | 0.190 秒 | 0.010 秒 |
备注:
- 使用了
g++ -O2
优化等级编译,其他条件相同。 - 性能提升显著:
pow<> v1
比未优化时快了约 47%pow<> v2
比未优化时快了约 90%
结论:
- 编译器优化大幅提升了模板版本的性能,尤其是
pow<> v2
,达到了极高的效率。 - 说明模板元编程结合现代优化可以极大减少运行时开销。
当进行模板元编程时…
- 要记住: 运行时(run-time) ≠ 编译时(compile-time)
因为模板元编程是在编译阶段执行的,所以不能依赖:- 可变性(mutability)
- 虚函数(virtual functions,不能动态绑定)
- 运行时类型信息(RTTI)等运行时特性
- 简单来说:
运行时代码是在程序运行时执行的,而模板元编程是在程序编译时执行的。
所以,模板元编程是在编译期写代码让编译器帮你做计算或生成代码,这限制了你能使用的语言特性。
如何将工作移到编译期(compile-time)
例子:一个计算整数绝对值的编译时元函数(metafunction)
template<int N> // 模板参数 N 作为元函数的参数
struct abs {static_assert(N != INT_MIN); // C++17 风格的编译期断言,防止溢出static constexpr auto value = (N < 0) ? -N : N; // 计算并“返回”结果
};
- 这里用模板参数
N
传入整数。 - 使用
static constexpr
定义value
,它在编译时即被求值。 static_assert
用来保证N
不是INT_MIN
(因为-INT_MIN
会溢出)。
使用方式
int const n = ...; // 或者直接声明为 constexpr
auto x = abs<n>::value; // 模板实例化,得到一个编译时常量
- 把参数作为模板参数传入。
- 通过
abs<n>::value
获取编译时计算结果。 - 这种写法使得绝对值计算在编译时完成,运行时直接使用结果,提升效率。
总结:
模板元编程让你写类似“函数”的模板,模板参数代表输入,value
代表输出,编译器在编译阶段就算好结果,运行时无需额外计算。
这段内容讲的是C++11中constexpr
函数与模板元函数(metafunction)两者的异同和优劣。
C++11 constexpr
函数示例
constexpr auto abs(int N) {return (N < 0) ? -N : N;
}
- 这是一个可以在编译时执行的函数(如果参数是编译时常量)。
- 使用方式很直观,就是普通函数调用:
abs(n)
。 - 如果
n
是constexpr
或编译时已知的常量,abs(n)
会在编译时求值。
模板元函数的优势(相比constexpr
函数)
模板元函数(结构体模板)除了能表达类似功能外,还有更多灵活强大的能力:
- 公有成员类型声明:可以定义
typedef
或using
类型别名,用来输出某种类型信息。 - 公有静态成员数据:
static const
或static constexpr
变量,可表示常量数据。 - 公有成员函数声明和定义:可以定义
constexpr
成员函数,进一步封装行为。 - 成员模板和编译期断言:支持嵌套模板和
static_assert
进行编译期检查,增强安全性和灵活度。
总结
constexpr
函数写起来更自然,调用更方便,适合简单的编译时计算。- 模板元函数结构体更“元”,不仅能计算值,还能定义类型、成员函数、断言等复杂编译时逻辑,是元编程更强大的工具。
两者各有用武之地,视场景需求而选择。
这是一个典型的使用模板元编程实现编译时递归计算最大公约数(GCD)的例子,利用了模板的偏特化作为递归终止(基底)条件。
代码解析
// 主模板,计算 gcd<M, N>,递归调用 gcd<N, M % N>
template<unsigned M, unsigned N>
struct gcd {static constexpr auto value = gcd<N, M % N>::value;
};// 偏特化,终止条件 gcd<M, 0>,此时结果为 M
template<unsigned M>
struct gcd<M, 0> {static_assert(M != 0, "gcd(0,0) undefined");static constexpr auto value = M;
};
- 主模板定义了递归步骤:
gcd<M, N> = gcd<N, M % N>
,类似欧几里得算法的递归形式。 - 偏特化模板定义了递归终止条件:当第二个参数是0时,最大公约数是第一个参数
M
,并且加了静态断言避免gcd(0,0)
的非法情况。 - 编译器在实例化模板时会根据参数匹配递归展开,最终在编译期算出结果。
使用示例
constexpr auto val = gcd<48, 18>::value; // val == 6
val
在编译时就能被计算为6。
这正是模板元编程的精髓:
- 编译时计算
- 递归展开
- 偏特化匹配基底
这个例子展示了如何写一个**元函数(metafunction)**来计算数组类型的维度(rank),即数组的“层数”。
代码解析
// 主模板,处理非数组类型,rank为0(基底情况)
template<class T>
struct rank {static constexpr size_t value = 0u;
};// 偏特化,匹配数组类型 U[N]
template<class U, size_t N>
struct rank<U[N]> {static constexpr size_t value = 1u + rank<U>::value;
};
- 主模板用于非数组类型,返回rank=0。
- 偏特化匹配数组类型,rank是数组的第一层1,再加上对数组元素类型
U
递归调用rank。 - 这样对多维数组
int[10][20][30]
,rank
会递归展开三层,结果是3。
使用示例
using array_t = int[10][20][30];
constexpr size_t r = rank<array_t>::value; // r == 3
r
是编译期常量,表示数组的维度。
总结
- 这是典型的模板元编程递归例子
- 利用偏特化来匹配数组类型,递归计算属性
- 运行时无需任何代码开销,全部在编译期完成
这是在讲 C++ 中 模板元函数(metafunction) 的另一个强大用途 —— 在编译期生成类型(type transformation)。
要点总结:
这个例子展示了如何定义一个元函数 remove_const
,它在编译期“去除”类型中的 const
修饰符,并返回一个新类型。
代码讲解:
1. 主模板(通用情况)
template< class T >
struct remove_const { using type = T;
};
- 如果传入的类型
T
没有const
修饰,就直接返回它自己。 type
是这个元函数的“返回值”。
2. 偏特化(专门处理 const
的情况)
template< class U >
struct remove_const< U const > { using type = U;
};
- 如果传入的是
U const
,就返回U
(即去掉const
)。
使用示例:
remove_const<int>::type a; // a 的类型是 int
remove_const<const int>::type b; // b 的类型也是 int(const 被去掉)
更现代的写法(C++14 起):
使用别名模板(alias template)更方便:
template<typename T>
using remove_const_t = typename remove_const<T>::type;
remove_const_t<const double> x; // x 是 double 类型
实用价值
- 编译期间完成类型处理,无需运行时开销。
- 常见于泛型编程、模板库(比如
<type_traits>
)。 - 类似的还有:
remove_reference
,add_pointer
,decay
,is_same
等。
小补充
这个例子与标准库中的 std::remove_const
完全一样逻辑:
#include <type_traits>
std::remove_const<const int>::type x; // x 是 int 类型
std::remove_const_t<const int> y; // C++14 简写
总结
元函数名 | 返回类型 | 用途 |
---|---|---|
remove_const<T> | 一个结构体,里面定义了 type 成员 | 编译期生成“去 const 的类型” |
remove_const_t<T> | typename remove_const<T>::type | 更简洁的别名,用于实际编程 |
这段内容讲的是 C++11 标准库元函数(metafunction)的一种命名约定(Convention #1),并举例说明如何通过这种约定实现一个 去除 volatile
限定符的类型变换模板。
核心概念解释
什么是 Metafunction(元函数)?
- 就是 在编译期间运行、用于计算或变换类型的模板结构(struct)。
- 元函数通常有一个成员
type
,表示其计算结果。
这个约定的内容是什么?
Convention #1:
如果一个模板元函数的结果是一个类型,则结果应命名为
type
。
这个规则在 C++11 开始普遍被采用(虽然早期如std::iterator_traits
不遵守这个规则,因为它较早出现)。
示例:type_is
—— 一个身份函数(identity metafunction)
template< class T >
struct type_is { using type = T;
};
- 接受类型
T
。 - 结果就是
T
自己。 - 它就是
std::type_identity<T>
的自定义版本。
用它来实现 remove_volatile
:
// 一般类型:不是 volatile,直接继承 type_is<T>
template< class T >
struct remove_volatile : type_is<T> { };// 特化:如果是 volatile 的类型,就返回不带 volatile 的版本
template< class U >
struct remove_volatile<U volatile> : type_is<U> { };
这样做的好处:
- 避免重复写
using type = ...;
。 - 统一接口:所有类型元函数都遵循
::type
访问结果。 - 可组合性好(多个元函数可以层层嵌套使用)。
使用示例:
remove_volatile<int>::type a; // a 是 int
remove_volatile<volatile double>::type b; // b 是 double
更简写(C++14 起):
template<typename T>
using remove_volatile_t = typename remove_volatile<T>::type;
remove_volatile_t<volatile int> c; // c 是 int
总结表
模板元函数 | 功能 | 使用方式 |
---|---|---|
type_is<T> | 恒等映射,返回类型 T | type_is<T>::type |
remove_volatile<T> | 去除类型中的 volatile 限定符 | remove_volatile<T>::type |
remove_volatile_t<T> | C++14 简写别名 | remove_volatile_t<T> |
这段内容讲的是 编译期决策(compile-time decision-making) —— 通过模板元函数,在编译阶段根据布尔常量选择不同的类型或路径,从而构建自适应(self-configuring)代码。
核心目标
定义一个模板元函数 IF
或别名 IF_t
,作用相当于三元表达式:
p ? T : F
在 模板上下文中使用它来根据条件 p
选择一个类型:如果 p
为 true
,返回类型 T
;否则返回类型 F
。
模板元函数实现
template<bool p, class T, class F>
struct IF : type_is<T> {}; // 默认选择 Ttemplate<class T, class F>
struct IF<false, T, F> : type_is<F> {}; // 特化选择 F// C++14 别名简化版本:
template<bool p, class T, class F>
using IF_t = typename IF<p, T, F>::type;
使用场景举例
int const q = ...; // 用户配置参数// 1. 声明变量 k,类型为 int 或 unsigned(根据 q 的值)
IF_t<(q < 0), int, unsigned> k;// 2. 实例化并调用某个函数对象(F 或 G)
IF_t<(q < 0), F, G>{}(args...);// 3. 派生类 D,继承自 B1 或 B2
class D : public IF_t<(q < 0), B1, B2> { ... };
编译期决策的价值
- 性能:没有运行时开销,因为所有决策在编译期已确定。
- 可读性/可维护性:代码清晰表达「选择逻辑」。
- 泛化/复用:适合用于模板库、配置选项、跨平台差异等。
延伸:标准库已有实现
标准库中其实早就有类似的东西:
#include <type_traits>std::conditional<p, T, F>::type
或 C++14 起的别名:
std::conditional_t<p, T, F>
你完全可以这样改写:
using IF_t = std::conditional_t<p, T, F>;
总结表
自定义名 | 等价标准库 | 说明 |
---|---|---|
IF<p, T, F> | std::conditional<p, T, F> | 返回 T 或 F ,基于布尔模板参数 |
IF_t<p, T, F> | std::conditional_t<p, T, F> | C++14 简写版本 |
是否需要我为你实现一个完整例子(带类继承或函数选择)来加深理解? |
SFINAE(Substitution Failure Is Not An Error) 的基本原理,尤其是它如何在 模板的隐式实例化(implicit template instantiation) 过程中生效。
什么是 SFINAE?
SFINAE 是 C++ 模板机制中的一个核心特性,全称是:
Substitution Failure Is Not An Error
意思是:在模板参数替换失败时,不报错,而是忽略该模板候选项。
编译器在模板实例化时做什么?
1. 推导模板参数(template argument deduction)
在使用模板时,编译器会尝试获取每个模板参数的具体类型。这些方式包括:
- 显式指定:用户直接提供模板参数,如
func<int>()
- 从函数参数推导:编译器从函数调用的实参中自动推断模板参数类型
- 默认模板参数:模板定义中有
= default_type
的默认值
2. 替换模板参数
然后,编译器将这些具体类型 替换模板参数,并尝试生成实际代码:
template<typename T>
void func(T t) {typename T::type x; // 可能出错
}
如果 T
没有 ::type
成员,那么 typename T::type
替换后就会是非法代码。
替换失败怎么办?
- 如果替换后代码合法,模板就被实例化,正常使用。
- 如果替换后代码不合法,不是错误! 编译器只是将这个模板候选项悄悄丢弃,不会报错。
这就是 SFINAE:替换失败不是错误,而是筛选机制的一部分。
举个例子:简单演示 SFINAE
// 只有当 T 有成员 type 时,此模板才合法
template<typename T>
auto test(int) -> typename T::type;template<typename>
auto test(...) -> void;struct A { using type = int; };
struct B { };int main() {test<A>(0); // 匹配第一个版本,因为 A 有 typetest<B>(0); // 匹配第二个版本,因为 B 没有 type,SFINAE 生效
}
总结要点
步骤 | 编译器行为 |
---|---|
① 推导模板参数 | 从调用上下文中获取具体类型 |
② 替换模板参数 | 替换模板中所有参数位置 |
替换后合法? | 继续实例化,模板有效 |
替换后非法? | 被编译器“静默丢弃”而非报错 —— 这就是 SFINAE |
SFINAE 的实际应用 ,使用 std::enable_if
来控制函数模板的实例化,从而选择不同版本的函数模板(即 条件重载)。我们来逐步解析。
目标
我们希望实现一个函数 f
,它根据参数类型 T
的不同执行不同的实现:
- 如果
T
是一个整数类型(如int
、long
),就使用第一个实现。 - 如果
T
是一个浮点数类型(如float
、double
),就使用另一个实现。 - 如果传入的类型既不是整数也不是浮点,比如
std::string
,则函数无定义,编译失败(两个版本都被 SFINAE 过滤掉了)。
使用工具:std::enable_if
这是标准库中用于实现 SFINAE 的工具:
template<bool B, class T = void>
struct enable_if {};template<class T>
struct enable_if<true, T> { using type = T; };
简写形式(从 C++14 开始):
template<bool B, class T = void>
using enable_if_t = typename enable_if<B, T>::type;
代码解释
整数版本:
template< class T >
enable_if_t< std::is_integral<T>::value, int >
f ( T val ) {// 只在 T 是整数类型时,这个版本才会参与编译return val * 2;
}
浮点版本:
template< class T >
enable_if_t< std::is_floating_point<T>::value, long double >
f ( T val ) {// 只在 T 是浮点类型时,这个版本才会参与编译return val + 0.5;
}
示例用法
f(10); // 调用整数版本,返回 int
f(3.14); // 调用浮点版本,返回 long double
f("text"); // 错误:两个模板都被 SFINAE 去除了,编译失败
提示:如何改进?
可以提供一个通用的 fallback 版本,以避免完全无定义的调用:
template<typename T>
void f(T) {static_assert(std::is_integral<T>::value || std::is_floating_point<T>::value,"f() only supports integral or floating-point types");
}
这样,如果调用 f("text")
,会得到一个更清晰的编译期错误信息,而不是“候选模板不匹配”的模糊提示。
Concepts Lite 的愿景及其如何简化当前复杂的模板元编程技术(如 SFINAE)的使用。我们逐步解析其含义,并用中文理解其重要性。
核心概念:Concepts Lite
Concepts Lite 是对 C++ 模板系统的一项重大增强,用于更自然地表达模板参数的约束条件。它使得模板接口更加清晰,并显著减少了 SFINAE 的冗长和不直观的语法。
对比 SFINAE vs Concepts
传统 SFINAE 写法
template<class T>
std::enable_if_t<std::is_integral<T>::value, int>
f(T val) {return val * 2;
}
优点:可实现类型约束
缺点:语法复杂、可读性差、编译错误难以理解
Concepts 写法(更现代、更清晰)
template<Integral T>
int f(T val) {return val * 2;
}
这里的 Integral
是一个 concept(概念),它大致相当于:
template<typename T>
concept Integral = std::is_integral_v<T>;
优点:
- 可读性强:直接表达“此模板参数必须是整数类型”
- 错误信息更友好
- 简化模板编写和重载管理
历史背景
- Concepts 概念起源于 C++ 模板的创始思想,主要由 Stepanov 推动(他也是 STL 的主要设计者)。
- 概念的数学灵感来自德国著名数学家 Emmy Noether,她推动了抽象代数的发展,强调用代数结构的共性来统一理论。
未来展望(2025视角)
- Concepts 已于 C++20 正式标准 中引入!
- 使用场景广泛,如 STL 泛型算法、模板库开发、类型安全设计等。
- 替代了很多以前用
enable_if
和 SFINAE 的代码,变得更清晰和易于维护。
小结:为什么 Concepts 很重要
特性 | 传统 SFINAE | Concepts(C++20) |
---|---|---|
可读性 | 差 | 非常高 |
编写复杂度 | 高 | 低 |
编译错误提示 | 难以理解 | 明确简洁 |
模板约束表达能力 | 间接 | 直接表达(语义化) |
如果你在学习现代 C++,掌握 Concepts 是理解模板设计的核心之一。 |
C++11 标准库元函数(metafunction)约定 #2,主要聚焦在“返回值”的表达方式。
C++11 元函数约定 #2:有值返回的 metafunction
规则说明
如果一个 metafunction 的返回值是一个值(value),而不是类型(type),它应该:
- 使用一个
static constexpr
成员value
来表示返回结果 - (可选)提供一些方便的类型定义或函数
示范:std::integral_constant
这是 C++11 中最具代表性的“值型元函数”结构:
template< class T, T v >
struct integral_constant {static constexpr T value = v;// 提供两个操作符重载:支持隐式转换和函数调用语法constexpr operator T() const noexcept { return value; }constexpr T operator()() const noexcept { return value; }// 其他成员通常不常用
};
示例用途
using five_t = std::integral_constant<int, 5>;
static_assert(five_t::value == 5, "Error");
five_t f;
int x = f(); // 相当于 x = 5;
int y = f; // 隐式转换:相当于 y = 5;
这个结构的妙处在于:
- 可以作为类型 使用:
five_t
是一个类型 - 也可以作为值 使用:
five_t::value
是一个编译时常量值 - 支持表达式语法:调用
f()
或赋值f
都可得到 5
派生技巧(Inheriting from integral_constant
)
通过从 integral_constant
继承,可以轻松创建自己的值型 metafunction,比如:
template<typename T>
struct is_void : std::integral_constant<bool, false> {};template<>
struct is_void<void> : std::integral_constant<bool, true> {};
这利用了 C++ 的继承机制:
- 可以自动继承
value
成员 - 可以使用
operator()
和隐式转换等特性 - 避免重复定义模板接口
小结
特性 | 说明 |
---|---|
value | 编译期常量值的标准访问接口 |
operator T() 和 operator() | 允许像值一样用 metafunction 类型 |
继承 integral_constant | 写值型 metafunction 的推荐方式 |
使用场景 | 用于类型特征(type traits)、条件选择、SFINAE 等 |
介绍了一个更完整、更现代化(基于 C++11)的 rank
元函数(metafunction)实现,用于在编译期计算一个数组类型的“维度”(即 rank,数组有多少层嵌套维度)。
理解目标:求数组类型的维度 rank
比如:
int a[2][3][4];
这个数组的 rank 是 3。
核心思想:用模板偏特化和递归实现计数
基本模板(标量类型)作为递归的基线:
template< class T >
struct rank : std::integral_constant<std::size_t, 0u> {};
说明:
- 如果传入的
T
不是数组(即标量),那 rank 为 0。 - 它继承自
std::integral_constant
,所以自动拥有::value
等便利接口。
偏特化 #1:有界数组(T[N]
)
template< class U, std::size_t N >
struct rank<U[N]> : std::integral_constant<std::size_t, 1u + rank<U>::value> {};
说明:
- 识别类型是
U[N]
时(固定长度数组) - 把 rank 递归地加 1,再对
U
调用rank
,以统计更深层次
偏特化 #2:无界数组(T[]
)
template< class U >
struct rank<U[]> : std::integral_constant<std::size_t, 1u + rank<U>::value> {};
说明:
- 识别类型是
U[]
(长度未知的数组,常用于函数参数中) - 一样加 1,继续递归
使用示例
using T1 = int;
using T2 = int[10];
using T3 = int[10][20];
using T4 = int[][30][40];static_assert(rank<T1>::value == 0, "T1不是数组");
static_assert(rank<T2>::value == 1, "T2是一维数组");
static_assert(rank<T3>::value == 2, "T3是二维数组");
static_assert(rank<T4>::value == 3, "T4是三维数组(首维无界)");
总结
模板形式 | 意义 |
---|---|
template<typename T> struct rank | 标量类型,rank 为 0 |
template<typename U, size_t N> struct rank<U[N]> | 有界数组类型,rank 加 1 |
template<typename U> struct rank<U[]> | 无界数组类型,rank 加 1 |
该写法利用了: |
- 模板递归
- 偏特化匹配不同的数组形式
std::integral_constant
提供一致的接口(::value
)
你提到的内容主要讲的是 integral_constant
及其衍生工具在 C++ 模板元编程中的便利性,以下是简要解析:
integral_constant
是什么?
它是一个用于在编译期传递常量值的模板结构:
template<class T, T v>
struct integral_constant {static constexpr T value = v;using value_type = T;using type = integral_constant; // type identityconstexpr operator value_type() const noexcept { return value; }constexpr value_type operator()() const noexcept { return value; }
};
常见别名:
template<bool b>
using bool_constant = integral_constant<bool, b>;
using true_type = bool_constant<true>;
using false_type = bool_constant<false>;
这些别名是许多类型特征(如 is_void
、is_integral
等)背后的基础构建块。
编译期调用方式演变:
以下 4 种方式都能从类型特征中提取布尔值:
方式 | 含义 |
---|---|
is_void<T>::value | 传统方式,取静态成员值 |
bool(is_void<T>{}) | 创建临时对象并转换为 bool(C++11) |
is_void<T>{}() | 创建对象并调用其 operator() (C++14) |
is_void_v<T> | 变量模板(C++14 起),更简洁(C++17 标准) |
示例
#include <type_traits>
#include <iostream>
template<bool B>
using bool_constant = std::integral_constant<bool, B>;
using true_type = bool_constant<true>;
using false_type = bool_constant<false>;
int main() {std::cout << std::boolalpha;std::cout << std::is_void<void>::value << '\n'; // truestd::cout << bool(std::is_void<int>{}) << '\n'; // falsestd::cout << std::is_void<void>{}() << '\n'; // true#if __cplusplus >= 201703Lstd::cout << std::is_void_v<void> << '\n'; // true
#endif
}
你提到的内容展示了 通过继承 + 偏特化的方式实现元函数(metafunction),这是 C++ 模板元编程中的常见技巧。以下是对两个例子的详细解释:
示例 1:判断类型是否为 void
// 基础模板:默认不是 void
template <class T>
struct is_void : false_type {};// 针对 void 的几个 cv 修饰版本做特化处理
template <> struct is_void<void> : true_type {};
template <> struct is_void<void const> : true_type {};
template <> struct is_void<void volatile> : true_type {};
template <> struct is_void<void const volatile> : true_type {};
原理:
- 利用全特化,识别
void
及其所有cv
修饰版本。 - 继承自
true_type
/false_type
(本质上是integral_constant<bool, true>
),表示布尔值。 - 元函数调用示例:
static_assert(is_void<void>::value); // OK static_assert(!is_void<int>::value); // OK
示例 2:判断两个类型是否相同(is_same
)
// 默认两个类型不一样
template <class T, class U>
struct is_same : false_type {};// 如果两个类型相同,偏特化匹配成功
template <class T>
struct is_same<T, T> : true_type {};
原理:
- 利用偏特化,当两个模板参数完全一致时,选择
is_same<T, T>
,继承true_type
。 - 否则使用默认模板,继承
false_type
。 - 示例:
static_assert(is_same<int, int>::value); // true static_assert(!is_same<int, float>::value); // false
总结关键点
技巧 | 用途 |
---|---|
模板继承 | 提供值类型元函数返回值 |
偏特化 / 全特化 | 进行“模式匹配”来选择不同实现 |
true_type/false_type | 简洁表达布尔类型元数据 |
你展示的是 类型萃取(type traits) 中一种 类型别名的高级用法,目的是通过 别名模板(alias template)= 类型委托 + 类型绑定 的方式实现更简洁、更易组合的元函数。
逐步解析:
目标:判断某类型是否为 void
template<class T>
using is_void = is_same<remove_cv_t<T>, void>;
这个别名的含义是:
- 去除
T
的 const 和 volatile 限定符后, - 判断该类型是否与
void
完全相同, - 返回结果为
is_same<去cv之后的T, void>
(继承自true_type
或false_type
)。
中间步骤定义:
1️remove_cv
:去除 const 和 volatile 限定
template<class T>
using remove_cv = remove_volatile<remove_const_t<T>>;
- 这表示:先用
remove_const_t<T>
去除 const,再交给remove_volatile<>
去除 volatile。 - 返回的是一个类型(
::type
还未取出)。
remove_cv_t
:取出最终类型
template<class T>
using remove_cv_t = typename remove_cv<T>::type;
- 这是标准 C++14 风格:为
remove_cv
的.type
添加简洁别名。 - 这样你可以直接写
remove_cv_t<T>
而不是typename remove_cv<T>::type
。
使用示例:
#include <type_traits>// 基础实现
template<class T>
using is_void = std::is_same<std::remove_cv_t<T>, void>;// 测试
static_assert(is_void<void>::value, "void");
static_assert(is_void<const void>::value, "const void");
static_assert(!is_void<int>::value, "int");
总结核心概念
名称 | 说明 |
---|---|
aliasing | 使用 using A = B<X> 实现类型别名 |
delegation | 将实际逻辑委托给已有类型(如 is_same ) |
binding | 利用别名 A<T> = B<处理后的T> 将类型“绑定”到最终实现 |
这种模式的优势是:可读性高、组合性强、编译期效率高,C++14 之后在 <type_traits> 中被广泛使用。 |
理解说明:Dispatching to best-performing algorithm
这段内容讲的是如何利用**类型萃取(type traits)和模板重载(function overloading)**在编译期选择最佳的算法实现,以提高性能。这种技巧也称为 tag dispatching(标签调度)。
背景:std::distance
的性能依赖于迭代器类型
C++ 中有不同种类的迭代器,每种的功能和性能差异很大:
迭代器类别 | 是否支持 e - b (随机访问) | 典型时间复杂度 |
---|---|---|
Input / Forward | 不支持 | O(N) |
Random Access Iterator | 支持 e - b | O(1) |
用 tag dispatching 区分两种实现:
// 适用于随机访问迭代器:可以用 e - b 实现 O(1)
template<class Iter>
auto distance(Iter b, Iter e, std::true_type) {return e - b;
}// 适用于非随机访问迭代器:必须遍历来计数,O(N)
template<class Iter>
auto distance(Iter b, Iter e, std::false_type) {typename std::iterator_traits<Iter>::difference_type d = 0;for (; b != e; ++b) ++d;return d;
}
分发入口:根据迭代器类型自动选择实现
template<class Iter>
inline auto distance(Iter b, Iter e) {// 判断 Iter 是否是随机访问迭代器类型using tag = std::is_same<typename std::iterator_traits<Iter>::iterator_category,std::random_access_iterator_tag>;return distance(b, e, tag{});
}
is_same<...>
返回true_type
或false_type
tag{}
会在编译期生成相应的类型对象- 然后根据 tag 选择合适的重载版本
这个模式的核心是:
步骤 | 说明 |
---|---|
1. 萃取类型信息 | 使用 iterator_traits<Iter>::iterator_category |
2. 编译期布尔判断 | is_same<...> 返回 true_type 或 false_type |
3. 标签调度 | 用类型标签选择合适的函数实现(函数重载) |
总结:为什么用这个技巧?
- 编译期选择最优实现,不依赖运行时判断,更快更安全。
- 可扩展性好,可以支持更多类型和策略。
- 标准库广泛使用,如
std::advance
,std::distance
等都用这种方式进行优化。
标准库常用的 迭代器标签分发(dispatching via iterator tags) 技术,目的是根据迭代器类型在编译期自动选择对应的算法实现,以实现性能优化。
关键点解析:
- 每个迭代器类型都通过
std::iterator_traits<Iter>::iterator_category
关联一个标签类型(tag type),如:std::random_access_iterator_tag
std::input_iterator_tag
- 其他迭代器标签
- 这些标签是空类型,仅用于类型匹配,方便通过函数重载选择实现。
代码示例说明:
template<class Iter>
auto distance(Iter b, Iter e, std::random_access_iterator_tag) {// 随机访问迭代器:直接用减法,O(1)return e - b;
}
template<class Iter>
auto distance(Iter b, Iter e, std::input_iterator_tag) {// 输入迭代器:只能顺序遍历,O(N)typename std::iterator_traits<Iter>::difference_type d = 0;for (; b != e; ++b) ++d;return d;
}
template<class Iter>
inline auto distance(Iter b, Iter e) {// 利用迭代器的类别标签调用合适的distance版本return distance(b, e, typename std::iterator_traits<Iter>::iterator_category{});
}
工作流程:
- 调用无标签版本
distance(b, e)
- 通过
iterator_traits
获取迭代器的类别标签 - 编译器根据标签调用对应重载函数实现
优点:
- 编译期选择最优算法,避免运行时开销。
- 代码结构清晰,易于扩展(比如支持其他迭代器类型)。
- 标准库中广泛使用这种标签调度模式。
这是用模板参数包(parameter pack)实现的变体的 is_same,叫做 is_one_of
,用来判断类型 T
是否和类型列表中的任意一个类型相同。
代码核心思路总结:
- primary template 只声明接口,不定义行为。
- base case 1:当类型列表为空,结果
false_type
,表示不匹配任何类型。 - base case 2:当列表头类型和
T
匹配,结果true_type
。 - 递归 case:当列表头类型和
T
不匹配,则递归检查列表剩余的类型。
完整示例代码:
#include <type_traits>// primary template: 只声明接口
template<class T, class... Types>
struct is_one_of;// base case: empty pack, no match
template<class T>
struct is_one_of<T> : std::false_type {};// match at head of list
template<class T, class... Tail>
struct is_one_of<T, T, Tail...> : std::true_type {};// mismatch at head, check tail recursively
template<class T, class Head, class... Tail>
struct is_one_of<T, Head, Tail...> : is_one_of<T, Tail...> {};
使用示例:
static_assert(is_one_of<int, char, int, float>::value, "int is in the list");
static_assert(!is_one_of<int, char, double, float>::value, "int is not in the list");
这段代码使用了刚才讲的 is_one_of
模板,将 is_void
实现为判断类型 T
是否属于四种 void
类型之一:
void
void const
void volatile
void const volatile
解释:
template<class T>
using is_void = is_one_of<T,void,void const,void volatile,void const volatile>;
- 这里用的是
using
关键字创建类型别名。 is_void<T>
实际上是调用is_one_of
,传入T
和 4 种void
变体。- 如果
T
是其中任意一种,is_one_of
会继承自std::true_type
,否则继承自std::false_type
。
使用示例:
static_assert(is_void<void>::value, "void is void");
static_assert(is_void<const void>::value, "const void is void");
static_assert(!is_void<int>::value, "int is not void");
你说的“unevaluated operands”(未求值操作数)指的是:sizeof
、alignof
、typeid
、decltype
和 noexcept
这些运算符的操作数表达式在编译时不会真正求值,也就是说:
- 它们不会触发代码生成。
- 只需要声明(declaration)就可以使用,不要求有定义(definition)。
举个关键用法例子:
#include <utility> // std::declvaltemplate<typename T>
auto foo(T&&) -> int;template<typename T>
using foo_return_t = decltype(foo(std::declval<T>()));
std::declval<T>()
是一个声明,模拟了类型T
的右值。decltype(foo(std::declval<T>()))
可以获取调用foo
时返回类型,但不会真的调用foo
。- 这样我们就可以在编译期推断类型,而无需实际构造对象或运行代码。
这个例子展示了如何用SFINAE检测一个类型 T
是否支持拷贝赋值操作。
关键点解析:
template<class T>
using copy_assign_t = decltype(declval<T&>() = declval<T const&>());
- 这是一个别名模板,用来表示
T
的拷贝赋值表达式的返回类型。 decltype
获取表达式declval<T&>() = declval<T const&>()
的类型(拷贝赋值的返回类型)。- 如果表达式无效(即
T
不支持拷贝赋值),decltype
会导致替换失败(SFINAE)。
template<class T>
struct is_copy_assignable {
private:template<class U, class = copy_assign_t<U>>static true_type try_assign(U&&);static false_type try_assign(...);
public:using type = decltype(try_assign(declval<T>()));
};
- 这是主模板结构体,检测
T
是否可拷贝赋值。 try_assign(U&&)
是一个模板函数,要求第二模板参数存在(即copy_assign_t<U>
有效),如果有效返回true_type
。- 另一重载
try_assign(...)
是捕获所有其他情况,返回false_type
。 type
使用decltype(try_assign(declval<T>()))
,即尝试调用try_assign
来判断T
是否支持拷贝赋值。
结论
- 如果
T
支持拷贝赋值,try_assign(U&&)
匹配成功,type
是true_type
。 - 否则,替换失败,
try_assign(...)
匹配,type
是false_type
。
你说的这个“老技术”在 C++11 引入 decltype
之前非常常见,利用 sizeof
运算符的不求值特性,通过函数重载返回不同大小的类型来判断某个表达式是否有效。
具体步骤:
-
定义两个不同大小的类型:
typedef char (&yes)[1]; // 类型大小为1字节 typedef char (&no)[2]; // 类型大小为2字节
-
重载检测函数:
- 当表达式合法时,返回类型为
yes
- 当表达式非法时,调用变参函数重载,返回类型为
no
- 当表达式合法时,返回类型为
-
用
sizeof
判断调用的重载版本:sizeof(try_assign(...))
- 如果返回
yes
,大小是1,表示表达式有效。 - 如果返回
no
,大小是2,表示表达式无效。
- 如果返回
-
用
bool_constant
包装结果:typedef bool_constant< sizeof(try_assign(...)) == sizeof(yes) > type;
这样就实现了一个编译期布尔值,用于判断表达式是否可用。
总结
- 这是 C++11
decltype
+ SFINAE 之前的经典技巧。 - 利用
sizeof
和函数重载实现编译期判断。 - 典型于 C++98/03 元编程代码。
为什么 void_t
重要?它的作用是什么?
void_t
是一个非常简单但功能强大的辅助模板别名,通常用于SFINAE技巧中判断某些类型表达式是否有效。
- 它的定义非常简单:
template<typename...>
using void_t = void;
- 也有一种为了绕开 CWG1558 问题(编译器对未使用模板参数的处理不一致)而定义的更安全版本:
template<typename...>
struct voider { using type = void; };
template<typename... Ts>
using void_t = typename voider<Ts...>::type;
void_t
怎么帮我们?
它把任意类型参数模板包映射成 void
,关键点:
- 传入的模板参数(可能是某些类型表达式)在编译期被实例化检测合法性。
- 如果传入的模板参数类型不合法,则产生替换失败(SFINAE),触发模板重载排除。
- 否则,
void_t<...>
别名替换为void
。
举个简单的例子
判断一个类型是否有成员类型 type
:
template<typename, typename = void>
struct has_type_member : std::false_type {};
template<typename T>
struct has_type_member<T, void_t<typename T::type>> : std::true_type {};
- 当
T::type
存在时,void_t<typename T::type>
合法,匹配偏特化,结果是true_type
。 - 当
T::type
不存在时,偏特化替换失败,退回到主模板,结果是false_type
。
总结
void_t
是编译期检查表达式合法性的惯用包装,简洁且强大。- 利用它,可以轻松写出通用的检测型 traits。
- 在 C++17 标准中,
void_t
已成为标准库的一部分。
你给的例子是 void_t
在检测类型成员是否存在的经典用法。
完整示范如下:
// ① 主模板,默认情况下没有 type 成员,继承 false_type
template<class, class = void>
struct has_type_member : std::false_type { };// ② 偏特化,当 T::type 合法时,使用 void_t 成功替换,继承 true_type
template<class T>
struct has_type_member<T, void_t<typename T::type>> : std::true_type { };
说明:
has_type_member<T>
默认是false_type
,表示没有type
成员。- 偏特化中,
void_t<typename T::type>
尝试提取T::type
,如果失败(T
没有type
),替换失败,SFINAE 规则作用,偏特化被排除。 - 如果成功,则匹配偏特化,继承
true_type
。
使用示例
struct A { using type = int; };
struct B { };
static_assert(has_type_member<A>::value, "A has type member"); // 通过
static_assert(!has_type_member<B>::value, "B has no type member"); // 通过
这就是 void_t
搭配 SFINAE 实现成员检测的典型用法:
- 主模板
has_type_member<T, void>
是默认版本,返回false_type
,表示类型T
不含type
成员。 - 偏特化 利用
void_t<typename T::type>
,若T::type
合法,则该特化被启用,返回true_type
。 - SFINAE 机制保证如果
T::type
不存在,偏特化无效,编译器自动选择主模板。
这种方式结构简洁,且不需要复杂的sizeof
、decltype
组合,非常适合现代 C++ 类型特征检测。
这里的设计思路是用 void_t
和 SFINAE 来检测类型 T
是否满足某种赋值操作(这里是“复制赋值”)的有效性:
详细解释:
- 辅助别名模板:
template< class T >
using copy_assign_t = decltype( std::declval<T&>() = std::declval<T const&>() );
- 该别名尝试生成表达式
T& = const T&
的类型(赋值表达式的返回类型)。 - 如果此表达式是有效的,则
copy_assign_t<T>
是有效的类型,否则无效。
- 主模板(处理不满足复制赋值的类型):
template< class T, class = void >
struct is_copy_assignable : std::false_type { };
- 默认情况下,类型不可复制赋值。
- 偏特化(处理满足复制赋值的类型):
template< class T >
struct is_copy_assignable< T, void_t< copy_assign_t<T> > > : std::true_type { };
- 当
copy_assign_t<T>
有效时,这个偏特化会被选中,继承自true_type
。
小结:
- SFINAE 原理:当
copy_assign_t<T>
无效,void_t<...>
替换失败,偏特化无效,编译器选择主模板(false_type
)。 - 当有效时,偏特化匹配更优,返回
true_type
。
扩展
如果你想检测 移动赋值,只需修改 copy_assign_t
中的右值引用版本:
template< class T >
using move_assign_t = decltype( std::declval<T&>() = std::declval<T&&>() );
完全理解!你这段代码是将检测“某种操作是否对类型有效”的SFINAE检测模式进一步抽象出来,用模板模板参数做通用操作包装,从而复用性更强。
* 模板模板参数 template<class> class Op
表示传入的是一个模板别名或类模板,接受一个类型参数,返回一个类型(或类型表达式)。
-
主模板:
template<class T, template<class> class Op, class = void> struct is_valid : std::false_type { };
默认情况下,类型
T
不满足操作Op
。 -
偏特化:
template<class T, template<class> class Op> struct is_valid<T, Op, void_t<Op<T>>> : std::true_type { };
当
Op<T>
是一个有效类型时(即Op<T>
可以被实例化),匹配此偏特化,说明T
支持操作Op
。 -
应用示例:
template<class T> using is_copy_assignable = is_valid<T, copy_assign_t>;template<class T> using is_move_assignable = is_valid<T, move_assign_t>;
优点
- 高度复用:只写一次
is_valid
,通过传入不同的操作模板别名,检查不同的特性。 - 简洁明了:代码更整洁,减少重复代码。
总结
这是一种典型的现代C++模板元编程设计范式,将操作检测封装成可传入的模板模板参数,再利用void_t
和SFINAE做有效性检测,极大提高了代码通用性和可维护性。
这段内容展示了不一定非得用 void_t
来做 SFINAE,也可以用 enable_if_t
来达到类似效果。
具体分析
-
主模板:
template<class T, class = void> struct is_signed : false_type {};
默认假设所有类型不是有符号类型。
-
偏特化:
template<class T> struct is_signed<T, std::enable_if_t<std::is_arithmetic<T>::value && (T(-1) < T(0)) >> : true_type {};
- 条件要求:
T
是算术类型(整型或浮点型)- 表达式
T(-1) < T(0)
为真 —— 这表明T
是有符号类型(因为-1小于0)。
- 只有满足这些条件,偏特化才成立,继承
true_type
。
- 条件要求:
-
静态断言测试:
static_assert(is_signed<long>::value); static_assert(!is_signed<unsigned>::value);
long
是有符号类型,断言通过。unsigned
不是有符号类型,断言也正确。
关键点
enable_if_t
也是一个void
类型的别名模板,但它附带条件判断,只在条件成立时生效。- 当条件不满足时,这个偏特化被 SFINAE 排除掉,退回主模板。
- 这种用法对写条件更灵活,特别是需要表达复杂逻辑时很方便。
总结
void_t
更简单,主要用作映射到void
,常用来检测类型成员存在与否。enable_if_t
允许带条件,能更灵活地做“条件限定”。- 两者都可以用来实现 SFINAE,但用途和场景有所区别。
你这段讲的是SFINAE技巧不用依赖void
或void_t
也能实现,核心是在两个地方都出现同一个“占位类型”,但实现原理一样:
核心点总结
- 需要同一个“占位类型”出现两处:
- 主模板的默认模板参数(
class = size_t
) - 偏特化的第二个模板参数,使用依赖于
T
的类型表达式(decltype(sizeof(T))
)
- 主模板的默认模板参数(
- 这里用的
decltype(sizeof(T))
:sizeof(T)
在编译期求值,但如果T
是不完整类型(比如void
),sizeof(T)
会编译错误。- SFINAE机制会令这偏特化不可行,选择主模板,结果为
false_type
。
具体例子说明
template<class T, class = size_t>
struct is_complete : false_type { };template<class T>
struct is_complete<T, decltype(sizeof(T))> : true_type { };// 测试:
static_assert(is_complete<long>::value); // long是完整类型
static_assert(!is_complete<void>::value); // void是不完整类型
is_complete<long>
会匹配偏特化,sizeof(long)
合法,结果是true_type
。is_complete<void>
偏特化SFINAE失败,匹配主模板,结果是false_type
。
这套路和用void_t
或enable_if_t
类似
- 只是替换了“占位类型”(
void
替换成了size_t
或decltype(sizeof(T))
)。 - 依然利用了SFINAE在模板参数替换阶段失败时“静默放弃”该偏特化。
void_t
的标准化过程就是C++标准委员会讨论的事情:
- **目标:**纳入C++17的
<type_traits>
,或者先以技术规范(TS)发布。 - **反馈:**普遍认为
void_t
很优雅,是SFINAE编程里的“神助攻”。 - **名字争议:**有很多候选名字,比如
make_void_t
、as_void_t
、voidify_t
、always_void
、enable_if_valid
等,反映出大家对名字的不同理解和偏好。
总结来说,void_t
是一个专门用来把任意一组合法类型映射成void
的模板别名,极大简化了SFINAE中的模板特化写法,也让元编程更直观、更易用。
- 元函数成员类型和静态 constexpr 数据成员:用来储存和传递编译期计算的结果,比如
value
、type
等。 - 元函数调用(递归可能)、继承、别名:把公共逻辑提取出来,减少重复,增强代码复用和可读性。
- 模板特化(完全和部分):通过匹配模板参数模式,区分不同类型的处理方式,实现分类元编程。
- SFINAE:用来优雅地排除不适用的模板候选,实现条件编译和重载决议。
- 未求值操作数:比如
sizeof
、decltype
、noexcept
等,用来查询类型信息而不产生代码。 - 参数包:表示类型列表的模板参数包,实现灵活的变长类型处理。
- 标准库元函数(如
<type_traits>
中的)和经典元函数:提供基础设施和算法,比如类型判断、迭代器特性、数值限制等。 void_t
和is_valid
习惯用法:现代SFINAE写法的利器,使得检测类型特性更加简洁明了。