CppCon 2020 学习:B2B:C++Templates Part2
可变参数模板使得函数或类模板可以接受不定数量的模板参数。sizeof...和参数包展开是关键技术点,支持递归操作和灵活编程。这使得模板编程更加灵活和强大。// 递归求最小值的可变参数模板函数a : b;// 递归展开参数包return m;int main()// 运行时测试// 输出 5// 输出 1return 0;// A: Normalize 重载,将不同类型转换成 std::string
可变参数模板(Variadic templates):参数包(Parameter pack)
- A
typename... Ts
或class... Ts
定义了一个类型模板参数包,可以包含任意数量的类型参数,Ts
是参数包的名字(可选)。 - B
Args... ts
定义了函数参数包,可以接受任意数量的函数参数,ts
是参数包的名字(可选)。 - C
sizeof...(ts)
用来计算参数包中有多少个参数,即参数的数量。 - D
ts...
在函数体中使用参数包展开,将包里的参数依次展开使用。
示例代码解析:
template<typename T, typename... Ts> // A:定义一个可变模板,包含至少一个类型T和任意多个Ts
constexpr auto min(const T& a, const T& b, const Ts&... ts) // B:函数参数包 ts
{
const auto m = a < b ? a : b;
if constexpr(sizeof...(ts) > 0) { // C:检查参数包是否还有参数
return min(m, ts...); // D:递归展开参数包,继续计算最小值
}
return m;
}
- 这个模板实现了一个递归计算最小值的函数,至少需要两个参数。
- 它先比较前两个参数
a
和b
,取较小的存入m
, - 如果还有更多参数(通过
sizeof...(ts)
判断),则递归调用min(m, ts...)
。 - 当参数包为空时,返回
m
,递归终止。
测试:
static_assert(min(3, 2, 3, 4, 5) == 2);
static_assert(min(3, 2) == 2);
- 编译时断言测试,确保函数能够正确计算最小值。
总结:
- 可变参数模板使得函数或类模板可以接受不定数量的模板参数。
sizeof...
和参数包展开是关键技术点,支持递归操作和灵活编程。- 这使得模板编程更加灵活和强大。
#include <iostream>
#include <cassert>
// 递归求最小值的可变参数模板函数
template<typename T, typename... Ts>
constexpr auto min(const T& a, const T& b, const Ts&... ts)
{
const auto m = (a < b) ? a : b;
if constexpr (sizeof...(ts) > 0) {
return min(m, ts...); // 递归展开参数包
}
return m;
}
int main()
{
static_assert(min(3, 2, 3, 4, 5) == 2);
static_assert(min(3, 2) == 2);
// 运行时测试
std::cout << "min(10, 5, 8, 7) = " << min(10, 5, 8, 7) << "\n"; // 输出 5
std::cout << "min(1, 2) = " << min(1, 2) << "\n"; // 输出 1
return 0;
}
#include <string>
#include <iostream>
#include <type_traits>
// A: Normalize 重载,将不同类型转换成 std::string
auto Normalize(const std::string& t) { return t; }
// 假设没有 Qt 库,这里简单注释掉 QString 版本
// auto Normalize(const QString& t) { return t.toStdString(); }
auto Normalize(const char* t) { return std::string{t}; }
// B: 模板版本,处理其它类型,调用 std::to_string
template<class T>
auto Normalize(const T& t) {
if constexpr (std::is_same_v<T, bool>) {
return t ? "true" : "false";
} else {
return std::to_string(t);
}
}
// C: 递归辅助函数,展开参数包拼接字符串
template<typename T, typename... Ts>
void _StrCat(std::string& ret, const T& targ, const Ts&... args)
{
ret += Normalize(targ);
if constexpr (sizeof...(args) > 0) {
_StrCat(ret, args...);
}
}
// D: 入口函数,从第一个参数开始递归拼接
template<typename T, typename... Ts>
auto StrCat(const T& targ, const Ts&... args)
{
std::string ret = Normalize(targ);
_StrCat(ret, args...);
return ret;
}
int main()
{
std::string s1 = StrCat("Hello ", std::string("world"), "!", 123, 4.56, true);
std::cout << s1 << "\n"; // 输出: Hello world!1234.561
return 0;
}
这段内容的理解如下:
可变参数模板(Variadic templates)
- 从 C++11 开始支持可变参数模板,它允许模板接受任意数量的参数。
- 这是对之前的可变参数宏(variadic macros)和可变参数函数(variadic functions)的一种更强大、更安全的泛型支持。
代码说明:
- A 定义了一些辅助函数
Normalize
,用于将各种类型转换为std::string
:const std::string&
直接返回QString
(假设是 Qt 的字符串类型)调用其转换函数const char*
转换为std::string
- B 定义了一个模板版本的
Normalize
,用于捕获其它类型,使用std::to_string
转换为字符串。
递归拼接字符串函数:
template<typename T, typename... Ts>
auto _StrCat(std::string& ret, const T& targ, const Ts&... args)
{
ret += Normalize(targ); // 先把当前参数转换成字符串拼接到 ret
if constexpr(sizeof...(args) > 0) {
_StrCat(ret, args...); // 递归调用,继续展开参数包
}
}
- C 利用
if constexpr
判断参数包是否为空,若不为空则继续递归展开。
入口函数:
template<typename T, typename... Ts>
auto StrCat(const T& targ, const Ts&... args)
{
std::string ret{Normalize(targ)}; // 先将第一个参数转换成字符串初始化结果字符串
_StrCat(ret, args...); // 递归调用拼接其余参数
return ret;
}
- D 入口函数,调用辅助递归函数完成所有参数的字符串拼接。
总结:
- 这段代码演示了如何用可变参数模板将任意数量、任意类型的参数转换为字符串并连接起来。
- 利用函数重载和模板特化,针对不同类型做不同的转换策略。
- 使用
if constexpr
保证递归展开安全、有效。
折叠表达式(Fold Expressions)
- 折叠表达式是 C++17 引入的,用来简化展开参数包(parameter pack)的操作。
- 它能代替递归模板函数,减少代码复杂度和编译时间。
折叠表达式的语法:
- 一元折叠(Unary folds)
- 右折叠(right fold):
(pack op ...)
- 左折叠(left fold):
(... op pack)
- 右折叠(right fold):
- 二元折叠(Binary folds)
- 右折叠:
(pack op ... op init)
- 左折叠:
(init op ... op pack)
- 右折叠:
- 注意:
op
必须是相同的操作符(比如+
,,
,&&
等)。- 表达式需要加括号来明确优先级。
代码示例解析:
#include <iostream>
template <typename T, typename... Ts>
void Print(const T& targ, const Ts&... args) {
std::cout << targ;
auto coutSpaceAndArg = [](const auto& arg) { std::cout << ' ' << arg; };
(..., coutSpaceAndArg(args)); // A:一元左折叠,依次调用 coutSpaceAndArg(args)
}
int main() { Print("Hello", "C++", 20); }
- 这个
Print
函数可以接受多个参数, - 先打印第一个参数
targ
, - 然后利用一元左折叠表达式
(..., coutSpaceAndArg(args))
, - 依次对剩余参数调用
coutSpaceAndArg
,实现依次打印,每个参数前加一个空格。 - 输出结果为:
Hello C++ 20
总结:
- 折叠表达式是模板展开参数包时的简洁利器。
- 它避免了递归调用,代码更简洁且编译速度更快。
- 常用于参数包的逐个处理,常见操作符是逗号
,
用来执行多个表达式。
#include <string>
#include <iostream>
#include <type_traits>
// 假设没有 Qt,先用简单替代
struct QString {
std::string str;
QString(const char* s) : str(s) {}
std::string toStdString() const { return str; }
};
// A Normalize函数重载,将不同类型转换为std::string
auto Normalize(const std::string& t) { return t; }
auto Normalize(const QString& t) { return t.toStdString(); }
auto Normalize(const char* t) { return std::string{t}; }
template<class T>
auto Normalize(const T& t) {
if constexpr (std::is_same_v<T, bool>)
return t ? "true" : "false";
else
return std::to_string(t);
}
// B 可变参数模板函数,拼接CSV格式字符串
template<class T, class... Ts>
auto BuildCSVLine(const T& targ, const Ts&... args)
{
auto ret{Normalize(targ)};
auto addCommaAndNormalize = [&](const auto& arg) {
ret += ',';
ret += Normalize(arg);
};
(..., addCommaAndNormalize(args)); // C 一元左折叠,展开参数包
return ret;
}
int main()
{
QString qs("@CppCon");
auto s = BuildCSVLine("Hello", std::string("C++"), 20, qs, true, 3.14);
std::cout << s << "\n"; // 输出: Hello,C++,20,@CppCon,true,3.140000
return 0;
}
这段内容的理解如下:
折叠表达式(Fold Expressions)示例讲解
A 部分:Normalize 函数(将不同类型转换为字符串)
auto Normalize(const std::string& t) { return t; }
直接返回字符串类型。auto Normalize(const QString& t) { return t.toStdString(); }
Qt 框架的QString
转为标准字符串。auto Normalize(const char* t) { return std::string{t}; }
将 C 风格字符串转为std::string
。template<class T> auto Normalize(const T& t) { return std::to_string(t); }
其它类型调用std::to_string
转为字符串。
B 部分:可变参数模板函数 BuildCSVLine
- 该函数用来拼接多个参数,生成逗号分隔的字符串(CSV格式)。
auto ret{Normalize(targ)};
将第一个参数转成字符串作为初始结果。- 定义 lambda
addColonAndNormalize
,用于在字符串中追加逗号和转换后的参数。
C 部分:折叠表达式展开参数包
(..., addColonAndNormalize(args));
是一元左折叠表达式,
它会依次调用addColonAndNormalize
,处理参数包args
中的每个参数,完成展开和拼接。
总结
- 利用折叠表达式,可以简洁、高效地展开参数包,避免递归。
- 通过多态的
Normalize
,支持多种类型统一转字符串。 - 该函数适合用于构建类似 CSV 格式的字符串,或日志格式化等。
可变参数模板:数据结构比较的示例(C++17)
主题背景
- 数据结构之间的比较通常是繁琐且容易出错的工作。
- 例如,比较两个 MAC 地址是否相等,或者检查 MAC 地址的所有字段是否都是某个特定值。
目标
- 实现简洁且安全的代码来进行这些比较。
代码示例
变参模板:数据结构比较(Variadic Templates: Data Structure Comparisons)
背景
- 对数据结构(如数组)进行比较的代码往往繁琐,且容易出错。
- 例如,检查一个 MAC 地址是否有效(所有字段是否符合某个条件)或两个 MAC 地址是否相等。
目标
- 设计两个
Compare
函数模板:- A:比较一个数组的所有元素是否都等于某个指定值。
- B:比较两个同样大小的数组是否每个元素都相等。
结构示例
struct MACAddress {
unsigned char value[6];
};
void Main() {
constexpr MACAddress macA{2, 2, 2, 2, 2, 2};
constexpr MACAddress macB{2, 2, 2, 2, 2, 4};
constexpr MACAddress macC{2, 2, 2, 2, 2, 2};
// A: 所有元素是否都等于 2
static_assert(Compare(macA.value, 2));
static_assert(!Compare(macB.value, 2));
// B: 两个数组是否完全相等
static_assert(!Compare(macA.value, macB.value));
static_assert(Compare(macA.value, macC.value));
}
C++17 实现细节
- 通过
std::index_sequence
和参数包展开实现编译时的数组元素访问。 - 利用折叠表达式
( ... && )
来比较所有元素。
namespace details::array_single_compare {
template<typename T, size_t N, typename U, size_t... I>
constexpr bool Compare(const T (&a)[N], const U& b, std::index_sequence<I...>) {
return ((a[I] == b) && ...); // 折叠表达式,判断每个元素是否等于 b
}
}
template<typename T, size_t N, typename U>
constexpr bool Compare(const T (&a)[N], const U& b) {
return details::array_single_compare::Compare(a, b, std::make_index_sequence<N>{});
}
namespace details::array_compare {
template<typename T, size_t N, size_t... I>
constexpr bool Compare(const T (&a)[N], const T (&b)[N], std::index_sequence<I...>) {
return ((a[I] == b[I]) && ...); // 折叠表达式,比较对应元素是否相等
}
}
template<typename T, size_t N>
constexpr bool Compare(const T (&a)[N], const T (&b)[N]) {
return details::array_compare::Compare(a, b, std::make_index_sequence<N>{});
}
C++20 改进写法(使用模板 lambda)
- 使用了 C++20 的模板 lambda,代码更简洁:
template<typename T, size_t N, typename U>
constexpr bool Compare(const T (&a)[N], const U& b) {
return [&]<size_t... I>(std::index_sequence<I...>) {
return ((a[I] == b) && ...);
}(std::make_index_sequence<N>{});
}
template<typename T, size_t N>
constexpr bool Compare(const T (&a)[N], const T (&b)[N]) {
return [&]<size_t... I>(std::index_sequence<I...>) {
return ((a[I] == b[I]) && ...);
}(std::make_index_sequence<N>{});
}
总结
- 这套方法巧妙结合了模板编程、折叠表达式和
std::index_sequence
实现了编译期数组的灵活比较。 - 大大简化了对固定大小数组的比较工作,避免手写循环错误。
- 同时代码具有编译期检查能力,提升了程序的安全性。
下面是结合你提供内容的完整可编译示例代码,实现了两种比较:
- 比较数组的所有元素是否都等于某个值
- 比较两个数组的对应元素是否都相等
#include <cstddef>
#include <utility> // for std::index_sequence
#include <type_traits>
// 细节命名空间:比较数组每个元素是否等于某个值
namespace details::array_single_compare {
template<typename T, size_t N, typename U, size_t... I>
constexpr bool Compare(const T (&a)[N], const U& b, std::index_sequence<I...>) {
return ((a[I] == b) && ...); // 折叠表达式,判断所有元素是否等于 b
}
}
// 比较数组所有元素是否都等于 b
template<typename T, size_t N, typename U>
constexpr bool Compare(const T (&a)[N], const U& b) {
return details::array_single_compare::Compare(a, b, std::make_index_sequence<N>{});
}
// 细节命名空间:比较两个数组对应元素是否相等
namespace details::array_compare {
template<typename T, size_t N, size_t... I>
constexpr bool Compare(const T (&a)[N], const T (&b)[N], std::index_sequence<I...>) {
return ((a[I] == b[I]) && ...); // 折叠表达式,比较对应元素是否相等
}
}
// 比较两个数组是否完全相等
template<typename T, size_t N>
constexpr bool Compare(const T (&a)[N], const T (&b)[N]) {
return details::array_compare::Compare(a, b, std::make_index_sequence<N>{});
}
// 示例结构体
struct MACAddress {
unsigned char value[6];
};
int main() {
constexpr MACAddress macA{{2, 2, 2, 2, 2, 2}};
constexpr MACAddress macB{{2, 2, 2, 2, 2, 4}};
constexpr MACAddress macC{{2, 2, 2, 2, 2, 2}};
static_assert(Compare(macA.value, 2)); // macA 所有元素都是 2,返回 true
static_assert(!Compare(macB.value, 2)); // macB 不全是 2,返回 false
static_assert(!Compare(macA.value, macB.value)); // macA 和 macB 不完全相等
static_assert(Compare(macA.value, macC.value)); // macA 和 macC 完全相等
return 0;
}
这段代码用到了 C++17 的折叠表达式和 std::index_sequence
,可以在编译期对固定大小数组进行元素比较,非常适合用于类似 MAC 地址这类固定长度数组的比较需求。
如果需要,我也可以帮你改写成 C++20 的模板 lambda 版本。需要的话告诉我!
拆开解释一下这段代码里 std::make_index_sequence
、std::index_sequence
、折叠表达式 ((a[I] == b) && ...)
是怎么配合工作的。
1. std::index_sequence
是什么?
它是 C++14 引入的一个模板类型,用来存储一系列编译期常量索引。
std::index_sequence<0, 1, 2>
意思就是一个类型,里面的值是 0、1、2。
它本身不存数据,只是模板参数的“列表容器”。
2. std::make_index_sequence<N>
是什么?
这是一个生成器,用来生成 0, 1, 2, ..., N-1
这样的 std::index_sequence
。
例如:
using seq = std::make_index_sequence<4>;
// 等价于 std::index_sequence<0, 1, 2, 3>
这样可以自动生成数组的所有索引,而不用手写 0、1、2、3。
3. 为什么要用它?
因为 C++ 不支持直接在模板参数中用 for
循环,但我们想对数组的每个元素做比较。
办法是:
- 生成一个从 0 到 N-1 的索引列表(
make_index_sequence
) - 把这些索引展开成多个比较语句(通过模板参数包展开)
4. (a[I] == b) && ...
是什么?
这是 C++17 的折叠表达式(fold expression)。
如果 I...
是 0, 1, 2, 3
,
((a[I] == b) && ...)
会展开成:
(a[0] == b) && (a[1] == b) && (a[2] == b) && (a[3] == b)
也就是逐个元素比较,并且用 &&
把结果连起来。
5. 结合流程来看
以 Compare(macA.value, 2)
为例:
constexpr bool Compare(const T (&a)[N], const U& b) {
return details::array_single_compare::Compare(a, b, std::make_index_sequence<N>{});
}
假设 N = 6
:
std::make_index_sequence<6>
生成std::index_sequence<0, 1, 2, 3, 4, 5>
- 传到
details::array_single_compare::Compare
- 模板参数
I...
变成0, 1, 2, 3, 4, 5
(a[I] == b) && ...
展开成:(a[0] == b) && (a[1] == b) && (a[2] == b) && (a[3] == b) && (a[4] == b) && (a[5] == b)
编译器在编译期就能算出这个结果,所以能放在 static_assert
里用。
6. 小图理解
std::make_index_sequence<6>
│
▼
std::index_sequence<0,1,2,3,4,5>
│
▼
(a[0] == b) && (a[1] == b) && ... && (a[5] == b)
│
▼
true 或 false (编译期计算)
如果你愿意,我可以帮你画一个这个 make_index_sequence
+ 折叠表达式的可视化执行过程图,这样你一看就明白了。
std::index_sequence<I...>
本质上是一个编译期的整数列表类型,
它唯一的用途就是把一串编译期整数打包成模板参数包,让你可以在模板里展开它们。
1. 它的定义(简化版)
在标准库里大概是这样的:
template<std::size_t... I>
struct index_sequence {};
没有成员变量,也没有逻辑,就是一个空壳类型,
唯一的作用是 把 I...
这串数字带在类型里。
2. 它长什么样
如果你写:
using seq = std::index_sequence<0, 1, 2, 3>;
那 seq
其实就是:
struct index_sequence<0, 1, 2, 3> {};
只是模板参数是 0, 1, 2, 3。
这个参数包 I...
在模板里可以被展开使用。
3. 为什么要它?
因为 C++ 模板里不能直接循环,但可以用模板参数包展开。
例如:
template<std::size_t... I>
void printIndex(std::index_sequence<I...>) {
((std::cout << I << " "), ...); // C++17 折叠表达式
}
调用:
printIndex(std::index_sequence<0, 1, 2>{});
展开后就是:
std::cout << 0 << " ";
std::cout << 1 << " ";
std::cout << 2 << " ";
4. 配合 std::make_index_sequence
std::make_index_sequence<N>
会自动生成 0, 1, ..., N-1
:
using seq = std::make_index_sequence<4>;
// 等价于 std::index_sequence<0, 1, 2, 3>
这样我们就可以在模板里用 I...
来代表数组下标,自动遍历数组。
5. 在你代码里的作用
在你的 Compare
里:
constexpr bool Compare(const T (&a)[N], const U& b) {
return details::array_single_compare::Compare(a, b, std::make_index_sequence<N>{});
}
std::make_index_sequence<N>
生成 std::index_sequence<0, 1, 2, ..., N-1>
然后传给:
template<typename T, size_t N, typename U, size_t... I>
constexpr bool Compare(const T (&a)[N], const U& b, std::index_sequence<I...>) {
return ((a[I] == b) && ...);
}
这里 I...
展开成所有下标,从而在编译期对每个元素做比较。
如果你愿意,我可以帮你画个 std::index_sequence
在你这段代码里的执行展开图,一步步展开到 (a[0] == b) && (a[1] == b) && ...
的样子,这样就能完全看懂了。
一个非常简单的例子给你演示 std::make_index_sequence
的用法,让你看到它是怎么把数字“展开”出来的。
例子:生成 0 到 N-1 并打印
#include <iostream>
#include <utility> // std::index_sequence, std::make_index_sequence
// 打印所有数字
template<std::size_t... I>
void printNumbers(std::index_sequence<I...>) {
// C++17 折叠表达式,把每个 I 展开打印
((std::cout << I << " "), ...);
}
int main() {
// 生成 0, 1, 2, 3, 4
printNumbers(std::make_index_sequence<5>{});
return 0;
}
编译器实际展开过程
调用:
printNumbers(std::make_index_sequence<5>{});
等价于:
printNumbers(std::index_sequence<0, 1, 2, 3, 4>{});
然后进入模板:
template<std::size_t... I>
void printNumbers(std::index_sequence<I...>) {
((std::cout << I << " "), ...);
}
展开后就是:
std::cout << 0 << " ";
std::cout << 1 << " ";
std::cout << 2 << " ";
std::cout << 3 << " ";
std::cout << 4 << " ";
运行结果:
0 1 2 3 4
总结:
std::make_index_sequence<N>
自动生成0, 1, ..., N-1
- 它经常用于编译期循环数组下标,让模板能对每个元素做操作
如果你愿意,我可以帮你用 make_index_sequence 写一个编译期 Compare的简化版,直接一步步展开成(a[0] == b) && (a[1] == b) ...
,这样你就能直观看到它在你原代码里是怎么工作的。
把这两段 C++14 的“变量模板(Variable Templates)”内容整理成理解,并顺便解释一下背后的原理。
1. 背景
在 C++14 之前,如果你想在模板元编程(TMP)里得到一个编译期常量值,通常需要通过结构体里的 ::value
来访问,比如:
static_assert(is_pointer<int*>::value);
这样写起来比较长,而且 ::value
不够直观。
C++14 引入了 变量模板(variable template),允许你直接定义一个模板变量来保存值,省去了 ::value
的访问,写法更简洁。
2. 第一版(C++11 风格)
// A: 编译期常量封装器
template<class T, T v>
struct integral_constant {
static constexpr T value = v; // 编译期常量
};
// B: 为 bool 类型定义两个别名
using true_type = integral_constant<bool, true>;
using false_type = integral_constant<bool, false>;
// C: 默认 is_pointer 模板,默认不是指针
template<class T>
struct is_pointer : false_type {};
// D: 针对指针类型的特化
template<class T>
struct is_pointer<T*> : true_type {};
// E: 使用时要写 ::value
static_assert(is_pointer<int*>::value); // OK
static_assert(not is_pointer<int>::value); // OK
这里的 is_pointer<T>
是个编译期类型信息类,要取结果必须 .value
。
3. 第二版(C++14 风格,变量模板)
// A: integral_constant 同上
template<class T, T v>
struct integral_constant {
static constexpr T value = v;
};
using true_type = integral_constant<bool, true>;
using false_type = integral_constant<bool, false>;
template<class T>
struct is_pointer : false_type {};
template<class T>
struct is_pointer<T*> : true_type {};
// B: 定义变量模板,直接暴露 ::value
template<typename T>
constexpr auto is_pointer_v = is_pointer<T>::value;
// C: 使用更简洁
static_assert(is_pointer_v<int*>); // 不用 ::value
static_assert(not is_pointer_v<int>);
4. 总结
integral_constant
是一个编译期常量包装器,常用在模板元编程中。true_type
/false_type
是它的两个常用别名。is_pointer
用来判断类型是否为指针,- 默认继承
false_type
(不是指针) - 对
T*
特化继承true_type
(是指针)
- 默认继承
- C++14 引入变量模板(
is_pointer_v
),让我们能用更直观的方式访问::value
。 - 变量模板写法在 TMP 中能大幅提升可读性和简洁度。
完整可运行的 C++14 示例代码,包含了 integral_constant
、true_type
/ false_type
、is_pointer
、is_pointer_v
的全部实现与测试。
// A: 编译期常量封装器
template<class T, T v>
struct integral_constant {
static constexpr T value = v; // 编译期常量
};
// B: 定义两个别名表示 true/false
using true_type = integral_constant<bool, true>;
using false_type = integral_constant<bool, false>;
// C: 默认 is_pointer 模板,默认不是指针
template<class T>
struct is_pointer : false_type {};
// D: 针对指针类型的特化
template<class T>
struct is_pointer<T*> : true_type {};
// E: C++14 变量模板,用来直接访问 ::value
template<typename T>
constexpr auto is_pointer_v = is_pointer<T>::value;
// F: 测试
int main() {
// C++11 写法:必须用 ::value
static_assert(is_pointer<int*>::value, "int* 应该是指针");
static_assert(!is_pointer<int>::value, "int 不是指针");
// C++14 写法:直接用 _v 版本
static_assert(is_pointer_v<int*>, "int* 应该是指针");
static_assert(!is_pointer_v<int>, "int 不是指针");
return 0;
}
编译:
g++ -std=c++14 test.cpp -o test && ./test
你给的这段是讲 C++ 模板里的 SFINAE 原理和一个简单例子,下面我帮你用逐行解释。
核心概念
- SFINAE 全称是 Substitution Failure Is Not An Error(替换失败并非错误)。
- 当编译器在尝试实例化模板时,如果某个特定类型代入后模板里的代码无法成立(比如不存在某个操作符、成员函数等),编译器不会立即报错,而是丢掉这个候选模板,继续找其它能匹配的模板。
- 如果最后没有任何匹配的模板,那才会编译错误。
- 这个特性让我们可以做条件重载、模板选择、启用/禁用特定版本的函数等。
例子解析
template<typename T>
bool equal(const T& a, const T& b)
{
return a == b; // 通用版本,直接用 ==
}
template<>
bool equal(const double& a, const double& b)
{
return std::abs(a - b) < 0.00001; // double 专用版本,比较时考虑浮点误差
}
void Main()
{
int a = 2;
int b = 1;
printf("%d\n", equal(a, b)); // 调用通用版本,直接比较
double d = 3.0;
double f = 4.0;
printf("%d\n", equal(d, f)); // 调用 double 特化版本
}
关键点
- 模板匹配规则
- 当
equal(a, b)
被调用时,编译器会从可用模板和特化中选择最合适的版本。 - 对于
int
→ 没有特化版本,使用通用模板。 - 对于
double
→ 存在全特化版本,所以直接用特化。
- 当
- SFINAE 的作用(这里没直接用到,但原理相同)
- 如果我们写一个模板版本在某些类型下无法编译,编译器会自动忽略它,然后找其它可用模板。
- 这样就可以不用手动写很多
template<>
特化,而是自动通过编译器的匹配规则选择。
- 为什么 float 没有特化
- 目前例子里 float 会走通用版本
a == b
。 - 如果想让 float 也用“近似比较”,我们可以再写一个
template<>
特化,或者用 SFINAE 自动为所有浮点类型启用“近似比较”。
- 目前例子里 float 会走通用版本
SFINAE 概念
- SFINAE = Substitution Failure Is Not An Error(替换失败并非错误)。
- 当编译器在尝试用某种类型实例化模板时,如果类型替换后导致编译失败(比如类型不支持某个操作),编译器不会直接报错,而是忽略这个模板版本,去尝试别的可用版本。
- 最终必须至少有一个模板版本能匹配成功,否则才会编译报错。
代码解析
#include <type_traits> // enable_if_t, is_floating_point_v
#include <cmath> // std::abs
#include <cstdio> // printf
template <typename T>
std::enable_if_t<not std::is_floating_point_v<T>, bool> equal(const T& a, const T& b) {
return a == b; // 对于非浮点类型,直接用 ==
}
template <typename T>
std::enable_if_t<std::is_floating_point_v<T>, bool> equal(const T& a, const T& b) {
return std::abs(a - b) < 0.00001; // 对于浮点类型,考虑精度误差
}
int main() {
int a = 2;
int b = 1;
printf("%d\n", equal(a, b)); // 调用第一个版本(非浮点)
double d = 3.0;
double f = 4.0;
printf("%d\n", equal(d, f)); // 调用第二个版本(浮点)
d = 4.0;
printf("%d\n", equal(d, f)); // 调用第二个版本(浮点)
}
关键点说明
std::enable_if_t
的作用std::enable_if_t<条件, 返回类型>
只有在条件为真时才会生成有效的函数签名,否则函数会被移出候选集(触发 SFINAE)。- 在这里,
std::is_floating_point_v<T>
是个编译期布尔值:- 对于
int
→false
- 对于
float/double/long double
→true
- 对于
- 第一个模板限制了“非浮点类型”才能实例化。
- 第二个模板限制了“浮点类型”才能实例化。
- 为什么不用特化而用 SFINAE
- 特化需要为每种浮点类型(float/double/long double)都写一个版本。
- SFINAE 可以用一个模板直接覆盖所有浮点类型。
- SFINAE 条件放的位置
- 这里放在返回类型位置,这样用户在调用时不需要看到额外的模板参数。
- 其他可选位置:
- 模板参数列表中:
template<typename T, typename = std::enable_if_t<条件>>
- 函数参数中:
equal(const T& a, const T& b, std::enable_if_t<条件>* = nullptr)
- 模板参数列表中:
- 调用过程
equal(a, b)
(int)→ 第一个模板匹配成功,第二个模板因is_floating_point_v<int>
为假而被移除。equal(d, f)
(double)→ 第二个模板匹配成功,第一个模板因条件不成立被移除。
这段内容主要是在讲 Tag Dispatch(标签派发) 的原理和用法,意思是:
理解
Tag Dispatch 是一种替代 SFINAE 的模板技术,用来在编译期选择不同的函数实现。
- 思路
- 定义几个空结构体(tag),用来代表不同的类型类别,例如:
struct notFloatingPoint {}; // 非浮点型标签 struct floatingPoint {}; // 浮点型标签
- 编写两个(或更多)实现逻辑不同的函数,这些函数参数表除了实际的参数,还多接收一个标签对象(tag object)。
- 在主接口里,用
if constexpr
或std::is_xxx
判断类型,再传入对应的标签对象,这样编译器就能在编译期选中正确的实现。
- 定义几个空结构体(tag),用来代表不同的类型类别,例如:
你的例子解析
namespace internal {
struct notFloatingPoint {};
struct floatingPoint {};
// 非浮点型版本
template<typename T>
bool equal(const T& a, const T& b, notFloatingPoint) {
return a == b;
}
// 浮点型版本
template<typename T>
bool equal(const T& a, const T& b, floatingPoint) {
return std::abs(a - b) < 0.00001;
}
} // namespace internal
template<typename T>
bool equal(const T& a, const T& b) {
using namespace internal;
if constexpr(std::is_floating_point_v<T>) {
return equal(a, b, floatingPoint{}); // 调用浮点版本
} else {
return equal(a, b, notFloatingPoint{}); // 调用非浮点版本
}
}
floatingPoint{}
和notFloatingPoint{}
是标签对象(tag object)。- 在
equal
主函数里,if constexpr
会在编译期根据类型选择调用哪个版本。 - 性能上是零开销的,因为编译器知道这个 tag 不会用到,会直接优化掉。
对比 SFINAE - SFINAE 用
enable_if_t
、is_xxx_v
在函数签名里做条件约束,让不匹配的模板被忽略。 - Tag Dispatch 用标签类型和重载来区分实现,逻辑更直观,也更容易读。
一个完整可运行的 Tag Dispatch 示例,包括浮点型和非浮点型的比较测试。
#include <iostream>
#include <type_traits>
#include <cmath>
namespace internal {
// 定义两个标签类型
struct notFloatingPoint {};
struct floatingPoint {};
// 非浮点型版本:直接比较
template<typename T>
bool equal(const T& a, const T& b, notFloatingPoint) {
std::cout << "[调用非浮点版本]\n";
return a == b;
}
// 浮点型版本:比较差值是否在允许范围内
template<typename T>
bool equal(const T& a, const T& b, floatingPoint) {
std::cout << "[调用浮点版本]\n";
return std::abs(a - b) < 0.00001;
}
} // namespace internal
// 主接口函数
template<typename T>
bool equal(const T& a, const T& b) {
using namespace internal;
if constexpr (std::is_floating_point_v<T>) {
// 如果是浮点类型,派发到浮点版本
return equal(a, b, floatingPoint{});
} else {
// 如果是非浮点类型,派发到非浮点版本
return equal(a, b, notFloatingPoint{});
}
}
int main() {
int x = 3, y = 3;
double m = 3.14159, n = 3.14158;
std::cout << "比较整数: " << equal(x, y) << "\n";
std::cout << "比较浮点: " << equal(m, n) << "\n";
return 0;
}
运行输出示例
[调用非浮点版本]
比较整数: 1
[调用浮点版本]
比较浮点: 0
这样你就可以清楚看到编译器根据 T
的类型自动选择了不同版本的 equal
。
如果你愿意,我还可以帮你写一个SFINAE 版本和这个 Tag Dispatch 版本放在一起,让你在同一份代码里对比两种方式的写法和调用效果。这样对比学习会很直观。
这段内容讲的是如何用Tag Dispatch和**Fold Expressions(折叠表达式)**来增强之前的可变参数模板拼接字符串函数,使其支持宽字符(wide strings)和普通字符串的转换与拼接。
理解
1. Tag Dispatch:用标签类型区分“普通字符串”和“宽字符串”
定义两个空结构体作为标签:
struct locale_s {}; // 普通字符串标签
struct locale_ws {}; // 宽字符串标签
然后针对这两个标签,重载 Normalize
函数(就是把各种类型转换成字符串的函数),区分普通字符串和宽字符串的处理:
- 普通字符串标签的
Normalize
:- 对
std::string
,直接返回 - 对
std::wstring
,调用to_string
转换为std::string
- 对
QString
、char*
等也做相应转换 - 对其他类型调用
std::to_string
- 对
- 宽字符串标签的
Normalize
:- 对
std::wstring
,直接返回 - 对
std::string
,调用to_wstring
转换为std::wstring
- 对
QString
、wchar_t*
等做相应转换 - 对其他类型调用
std::to_wstring
- 对
2. 使用可变模板参数和折叠表达式拼接字符串
template<class LT = locale_s, class T, class... Ts>
auto BuildCSVLine(const T& targ, const Ts&... args)
{
auto ret{Normalize(targ, LT{})}; // 用标签区分调用不同Normalize
auto addColonAndNormalize = [&](const auto& arg) {
ret += ',';
ret += Normalize(arg, LT{});
};
(..., addColonAndNormalize(args)); // 折叠表达式遍历剩余参数,拼接字符串
return ret;
}
这个函数默认用普通字符串标签 locale_s
,也可以传入宽字符串标签 locale_ws
,决定调用哪个 Normalize
版本。
3. 宽字符串版本的方便接口
template<class... Ts>
auto BuildWCSVLine(const Ts&... args)
{
return BuildCSVLine<locale_ws>(args...);
}
直接调用时不需要传标签,默认构造宽字符串的 CSV 拼接。
总结:
- Tag Dispatch 用空结构体标签类型区分不同的行为(普通字符串 VS 宽字符串)。
- Fold Expressions 用
(..., expr)
语法轻松展开参数包,实现循环效果。 - 这样实现的代码干净优雅,支持多种字符串类型拼接,且复用度高。
#include <iostream>
#include <string>
// 假设的 QString 类型简化示例
struct QString {
std::string s;
QString(const char* str) : s(str) {}
std::string toStdString() const { return s; }
std::wstring toStdWString() const { return std::wstring(s.begin(), s.end()); }
};
// 假设的 to_string 和 to_wstring 实现
std::string to_string(const std::wstring& ws) { return std::string(ws.begin(), ws.end()); }
std::wstring to_wstring(const std::string& s) { return std::wstring(s.begin(), s.end()); }
// 标签类型,用于区分普通字符串和宽字符串
struct locale_s {};
struct locale_ws {};
// 普通字符串版本 Normalize
auto Normalize(const std::string& t, locale_s) { return t; }
auto Normalize(const std::wstring& t, locale_s) { return to_string(t); }
auto Normalize(const QString& t, locale_s) { return t.toStdString(); }
auto Normalize(const char* t, locale_s) { return std::string{t}; }
auto Normalize(const wchar_t* t, locale_s) { return Normalize(std::wstring{t}, locale_s{}); }
template <class T>
auto Normalize(const T& t, locale_s) {
return std::to_string(t);
}
// 宽字符串版本 Normalize
auto Normalize(const std::string& t, locale_ws) { return to_wstring(t); }
auto Normalize(const std::wstring& t, locale_ws) { return t; }
auto Normalize(const QString& t, locale_ws) { return t.toStdWString(); }
auto Normalize(const char* t, locale_ws) { return Normalize(std::string{t}, locale_ws{}); }
auto Normalize(const wchar_t* t, locale_ws) { return std::wstring{t}; }
template <class T>
auto Normalize(const T& t, locale_ws) {
return std::to_wstring(t);
}
// 变参模板拼接 CSV 行,默认普通字符串标签 locale_s
template <class LT = locale_s, class T, class... Ts>
auto BuildCSVLine(const T& targ, const Ts&... args) {
auto ret{Normalize(targ, LT{})};
auto addColonAndNormalize = [&](const auto& arg) {
if constexpr (std::is_same_v<LT, locale_s>) {
ret += ',';
} else {
ret += L',';
}
ret += Normalize(arg, LT{});
};
(..., addColonAndNormalize(args)); // 折叠表达式
return ret;
}
// 方便调用宽字符串版本
template <class... Ts>
auto BuildWCSVLine(const Ts&... args) {
return BuildCSVLine<locale_ws>(args...);
}
int main() {
QString qs("QtString");
auto csv = BuildCSVLine("Hello", std::string{"C++"}, 20, qs);
std::cout << "CSV: " << csv << '\n';
auto wcsv = BuildWCSVLine(L"Wide", std::wstring{L"String"}, 123, qs);
std::wcout << L"WCSV: " << wcsv << L'\n';
return 0;
}
说明:
- 这段代码用
locale_s
和locale_ws
区分普通和宽字符字符串处理。 - 利用可变模板参数和折叠表达式拼接带逗号分隔的字符串。
QString
、to_string
和to_wstring
是示例简化,真实环境用对应库函数。BuildCSVLine
默认拼接普通字符串,BuildWCSVLine
拼接宽字符串。- 逗号分隔符根据标签用
','
或L','
。
requires (C++20 中的 Concepts)
- C++20 引入了 Concepts,可以用
requires
替代 SFINAE,让模板约束更清晰易懂。 - Concepts 是一套模板约束的表达方式,它不仅功能更强大,而且让代码更干净、更有表达力。
示例代码:
#include <type_traits>
#include <cmath>
#include <cstdio>
template <typename T>
requires(not std::is_floating_point_v<T>)
bool equal(const T& a, const T& b) {
return a == b;
}
template <typename T>
requires(std::is_floating_point_v<T>)
bool equal(const T& a, const T& b) {
return std::abs(a - b) < 0.00001;
}
int main() {
int a = 2, b = 1;
printf("%d\n", equal(a, b)); // 非浮点类型,直接比较
double d = 3.0, f = 4.0;
printf("%d\n", equal(d, f)); // 浮点类型,近似比较
}
Template Template Parameters(模板模板参数)
- 模板模板参数是指模板的模板参数,即参数本身也是模板。
- 用于传入模板类(如容器类),使得模板更灵活。
- 语法中要先声明模板模板参数(用
template<class, class> class Container
之类),然后在函数或类模板中使用。
示例代码:
#include <vector>
#include <list>
#include <cstdio>
template <template <class, class> class Container, // 模板模板参数:传入一个模板类,带两个模板参数
class T,
class Allocator = std::allocator<T> // 默认模板参数
>
void Fun(const Container<T, Allocator>& c) {
for (const auto& e : c) {
printf("%d\n", e);
}
}
int main() {
std::vector<int> v{2, 3, 4};
Fun(v); // vector 满足 Container<T, Allocator> 结构
std::list<char> l{'a', 'b', 'c'};
Fun(l); // list 同样满足
}
- 这里
Container
可以是std::vector
、std::list
等模板类,它们有两个模板参数(元素类型,分配器类型)。 Fun
函数模板接受任何符合这个模板模板参数格式的容器。
总结:- Concepts (
requires
) 让模板约束语法更简洁。 - 模板模板参数用于接受“模板类”作为模板参数,增强模板的泛用性。
好的,我帮你详细解释这段模板模板参数的声明:
template <
template <class, class> class Container, // 模板模板参数
class T,
class Allocator = std::allocator<T> // 默认模板参数
>
void Fun(const Container<T, Allocator>& c) {
// ...
}
逐部分拆解:
1. template <template <class, class> class Container, ... >
- 这是模板模板参数的语法。
- 意思是:
Container
是一个模板类,它本身是带有两个模板参数的模板。
举个例子: std::vector
是模板类,定义是template<class T, class Allocator = std::allocator<T>> class vector { ... }
,它有两个模板参数,第一个是元素类型,第二个是分配器类型。std::list
同样是带两个模板参数的模板类。
所以template<class, class> class Container
表示的是这样一个模板类:带两个类型参数。
2. class T
- 这是给
Container
模板传入的第一个类型参数,通常是容器的元素类型。
3. class Allocator = std::allocator<T>
- 这是给
Container
模板传入的第二个类型参数,有默认值,是元素类型T
对应的标准分配器。 - 这个参数带有默认值,也就是说如果调用函数时没有显示指定分配器,会自动使用
std::allocator<T>
。
总结理解:
Container
是一个模板模板参数,代表任何带两个模板参数(类型)的模板类。- 你调用这个函数时,可以传入像
std::vector<int>
、std::list<char>
等容器。 T
是元素类型。Allocator
是分配器类型,默认是std::allocator<T>
。
更多推荐
所有评论(0)