可变参数模板(Variadic templates):参数包(Parameter pack)

  • A typename... Tsclass... 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;
}
  • 这个模板实现了一个递归计算最小值的函数,至少需要两个参数。
  • 它先比较前两个参数 ab,取较小的存入 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)
  • 二元折叠(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_sequencestd::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 循环,但我们想对数组的每个元素做比较。
办法是:

  1. 生成一个从 0 到 N-1 的索引列表(make_index_sequence
  2. 把这些索引展开成多个比较语句(通过模板参数包展开)

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. 总结

  1. integral_constant 是一个编译期常量包装器,常用在模板元编程中。
  2. true_type / false_type 是它的两个常用别名。
  3. is_pointer 用来判断类型是否为指针,
    • 默认继承 false_type(不是指针)
    • T* 特化继承 true_type(是指针)
  4. C++14 引入变量模板is_pointer_v),让我们能用更直观的方式访问 ::value
  5. 变量模板写法在 TMP 中能大幅提升可读性简洁度

完整可运行的 C++14 示例代码,包含了 integral_constanttrue_type / false_typeis_pointeris_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 特化版本
}

关键点

  1. 模板匹配规则
    • equal(a, b) 被调用时,编译器会从可用模板和特化中选择最合适的版本。
    • 对于 int → 没有特化版本,使用通用模板。
    • 对于 double → 存在全特化版本,所以直接用特化。
  2. SFINAE 的作用(这里没直接用到,但原理相同)
    • 如果我们写一个模板版本在某些类型下无法编译,编译器会自动忽略它,然后找其它可用模板。
    • 这样就可以不用手动写很多 template<> 特化,而是自动通过编译器的匹配规则选择。
  3. 为什么 float 没有特化
    • 目前例子里 float 会走通用版本 a == b
    • 如果想让 float 也用“近似比较”,我们可以再写一个 template<> 特化,或者用 SFINAE 自动为所有浮点类型启用“近似比较”。

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));  // 调用第二个版本(浮点)
}

关键点说明

  1. std::enable_if_t 的作用
    • std::enable_if_t<条件, 返回类型> 只有在条件为真时才会生成有效的函数签名,否则函数会被移出候选集(触发 SFINAE)。
    • 在这里,std::is_floating_point_v<T> 是个编译期布尔值:
      • 对于 intfalse
      • 对于 float/double/long doubletrue
    • 第一个模板限制了“非浮点类型”才能实例化。
    • 第二个模板限制了“浮点类型”才能实例化。
  2. 为什么不用特化而用 SFINAE
    • 特化需要为每种浮点类型(float/double/long double)都写一个版本。
    • SFINAE 可以用一个模板直接覆盖所有浮点类型。
  3. SFINAE 条件放的位置
    • 这里放在返回类型位置,这样用户在调用时不需要看到额外的模板参数。
    • 其他可选位置:
      • 模板参数列表中:template<typename T, typename = std::enable_if_t<条件>>
      • 函数参数中:equal(const T& a, const T& b, std::enable_if_t<条件>* = nullptr)
  4. 调用过程
    • 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 constexprstd::is_xxx 判断类型,再传入对应的标签对象,这样编译器就能在编译期选中正确的实现。

你的例子解析

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
  • SFINAEenable_if_tis_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
    • QStringchar* 等也做相应转换
    • 对其他类型调用 std::to_string
  • 宽字符串标签的 Normalize
    • std::wstring,直接返回
    • std::string,调用 to_wstring 转换为 std::wstring
    • QStringwchar_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_slocale_ws 区分普通和宽字符字符串处理。
  • 利用可变模板参数和折叠表达式拼接带逗号分隔的字符串。
  • QStringto_stringto_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::vectorstd::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>
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐