C++ 泛型编程指南 可变参数模板2

https://blog.csdn.net/qq_55125921/article/details/128886542?fromshare=blogdetail&sharetype=blogdetail&sharerId=128886542&sharerefer=PC&sharesource=qq_55125921&sharefrom=from_link


1 介绍

可变参数模板是一种可以接受任意数量模板参数的模板。

1.1 示例

可以使用 print() 来处理可变类型的参数:

#include <iostream>

void print() {}

template<typename T, typename... Types>
void print(T firstArg, Types... args) {
    std::cout << firstArg << '\n';  // 打印第一个参数
    print(args...);                // 递归调用 print() 以处理剩余的参数
}

当传递一个或多个参数时,使用函数模板,通过指定第一个参数,然后在递归调用其余参数的 print() 前,打印第一个参数。剩下的 args 是一个函数参数包:

void print(T firstArg, Types... args)

使用由模板参数包指定的不同“类型”:

template<typename T, typename... Types>

为了结束递归,需要提供 print() 的非模板重载,当参数包为空时使用。

例如,

std::string s("world");
print(7.5, "hello", s);

将输出以下内容:

7.5
hello
world

调用首先展开为:

print<double, char const*, std::string>(7.5, "hello", s);
  • firstArg 的值为 7.5,因此 Tdouble 类型
  • args 是可变参数,其值为 char const* 型的 "hello"std::string 型的 "world"

打印 7.5 作为 firstArg 后,再次对其余参数调用 print(),然后展开为:

print<char const*, std::string>("hello", s);
  • firstArg 的值为 "hello",所以 T 的类型是 char const*
  • args 是可变参数,其值类型为 std::string

打印 "hello" 作为 firstArg 后,再次对其余参数调用 print(),然后展开为:

print<std::string>(s);
  • firstArg 的值为 "world",所以类型 T 现在是 std::string
  • args 是空的可变参数模板参数

因此,在打印 "world" 作为 firstArg 之后,调用 print() 时不带参数,这将调用 print() 的非模板重载。

完整代码

#include <iostream>
#include <string>

// 非模板重载的 print() 函数,用于结束递归
void print() {
 std::cout << "空参数函数" <<  '\n';  
    // 空函数体,不需要执行任何操作
}

// 模板函数,可以接受一个或多个参数
template<typename T, typename... Types>
void print(T firstArg, Types... args) {
    std::cout << firstArg << '\n';  // 打印第一个参数
    print(args...);                // 递归调用 print() 以处理剩余的参数
}

int main() {
    std::string s("world");

    // 调用 print() 函数,传入多个参数
    print(7.5, "hello", s);

    return 0;
}
7.5
hello
world
空参数函数

1.1.2 重载可变和非可变模板

#include <iostream>

// 非变参模板函数,用于打印单个参数
template<typename T>
void print(T arg) {
    std::cout <<"非变参模板函数" << arg << '\n'; // 打印传入的参数
}

// 变参模板函数,用于打印多个参数
template<typename T, typename... Types>
void print(T firstArg, Types... args) {
    print(firstArg); // 调用非变参模板函数打印第一个参数
    print(args...);  // 递归调用自身处理剩余的参数
}

int main() {
    print(7.5, "hello", std::string("world")); // 测试 print 函数
    return 0;
}
非变参模板函数7.5
非变参模板函数hello
非变参模板函数world

若两个函数模板的区别仅在于末尾参数包的不同,则首选没有末尾参数包的函数模板。

代码说明

  • 非变参模板函数 print(T arg):这个函数模板只接受一个参数,用于打印单个参数。它用于在调用过程中结束递归。

  • 变参模板函数 print(T firstArg, Types... args):这个函数模板可以接受一个或多个参数。首先打印第一个参数,然后递归调用自身以处理其余的参数。每次递归调用都减少一个参数,最终会调用到非变参模板函数,结束递归。

    重载规则说明

  • 当调用 print() 函数时,如果传递的是单个参数,编译器会选择非变参模板函数。

  • 如果传递了多个参数,则会调用变参模板函数,并逐步递归处理多余的参数。

  • 这种设计利用了重载解析的规则,可以在模板函数中实现递归式的参数包处理。

1.1.2 递归参数打印模式

#include <iostream>

// 变参模板函数的帮助函数,用于处理没有变参的情况
template<typename T>
void print_impl(T firstArg) {
    std::cout << "变参模板帮助函数: " << firstArg << '\n'; // 打印传入的参数
}

// 变参模板函数,用于打印多个参数
template<typename T, typename... Types>
void print_impl(T firstArg, Types... args) {
    std::cout << "变参模板函数: " << firstArg << '\n'; // 打印第一个参数
    print_impl(args...);  // 递归调用处理剩余的参数
}

// 包含伪参数的变参模板函数,确保始终调用变参版本
template<typename... Types>
void print(Types... args) {
    print_impl(args...);  // 调用真实的变参实现
}

int main() {
    print(7.5, "hello", std::string("world")); // 测试 print 函数
    print(42); // 测试调用单个参数
    return 0;
}

输出是这样的原因与变参模板的设计以及递归的实现方式有关。在你的代码中,有两个版本的 print_impl 函数,一个是用于处理单个参数的基础情况,另一个是用于处理多个参数的递归情况。以下是这个输出的具体原因:

  1. 第一个参数 7.5 的处理:

    • 当你调用 print(7.5, "hello", std::string("world")); 时,print 函数一开始将这些参数传递给 print_impl
    • print_impl 的多参数版本接收第一个参数 7.5 并打印 变参模板函数: 7.5,然后递归调用自己并传递剩余的参数 "hello""world"
  2. 第二个参数 "hello" 的处理:

    • 递归调用进入,print_impl 的多参数版本再次接收第一个参数 "hello",打印 变参模板函数: hello
    • 剩下的参数包中只有一个参数,即 std::string("world"),因此它再次递归调用自己并传递这个参数。
  3. 最后一个参数 world 的处理:

    • 递归调用进入时,这次 print_impl 只有一个参数 std::string("world")
    • 因为只有一个参数,调用匹配的是print_impl的单参数版本。
    • 这时,打印 变参模板帮助函数: world,因为已经没有更多参数可以递归,处理到此结束,这也是递归的终止条件。

总结

  • 有两个 print_impl 函数,一个专门用于处理单个元素(基础情况),另一个用于处理多个(递归),这解决了递归的结束问题。
  • 每次递归时,先处理并打印当前的第一个参数,然后继续递归调用,逐步减少处理的参数数量,直到只剩下一个参数,调用单参数版本完成最后的打印。
  • 因此,这种操作顺序导致了“7.5”、“hello”和“world”的逐步处理和输出。

1.1.4 sizeof... 运算符

C++11 为变参模板引入了一种新的 sizeof 运算符:sizeof...。它会被扩展成参数包中所包含的参数数目。因此:

template<typename T, typename... Types>
void print(T firstArg, Types... args) {
    std::cout << firstArg << '\n'; // Print first argument
    std::cout << sizeof...(Types) << '\n'; // Print number of remaining types
    std::cout << sizeof...(args) << '\n'; // Print number of remaining args
}

在将第一个参数打印之后,会将参数包中剩余的参数数目打印两次。如你所见,运算符 sizeof... 既可以用于模板参数包,也可以用于函数参数包。

这样可能会让你觉得,可以不使用为了结束递归而重载的不接受参数的非模板函数 print(),只要在没有参数的时候不去调用任何函数就可以了:

template<typename T, typename... Types>
void print(T firstArg, Types... args) {
    std::cout << firstArg << '\n';
    if (sizeof...(args) > 0) { // Error if sizeof...(args) == 0
        print(args...); // And no print() for no arguments declared
    }
}

但是这一方式是错误的,因为通常函数模板中 if 语句的两个分支都会被实例化。是否使用被实例化出来的代码是在运行期间(runtime)决定的,而是否实例化代码是在编译期间(compile-time)决定的。因此如果在只有一个参数的时候调用 print() 函数模板,虽然 args... 为空,if 语句中的 print(args...) 也依然会被实例化,但此时没有定义不接受参数的 print() 函数,因此会报错。

不过从 C++17 开始,可以使用编译阶段的 if 语句,这样通过一些稍微不同的语法,就可以实现前面想要的功能。8.5 节会对这一部分内容进行讨论。

完整的代码

当然,下面是一个完整的 C++ 示例,演示如何使用 sizeof... 运算符来处理变参模板。这个示例中,我们将使用一个递归的函数模板来打印所有参数并展示每一步中剩余参数的数量:

#include <iostream>

// 一个可变参数模板函数,用于打印参数和参数数量
template<typename T, typename... Types>
void print(T firstArg, Types... args) {
    // 打印第一个参数
    std::cout << "Argument: " << firstArg << '\n';
    
    // 使用 sizeof... 打印剩余的参数数量
    std::cout << "Number of remaining arguments (Types): " << sizeof...(Types) << '\n';
    std::cout << "Number of remaining arguments (args): " << sizeof...(args) << '\n';

    // 递归调用自身打印剩余的参数
    if constexpr (sizeof...(args) > 0) {
        print(args...);
    }
}

int main() {
    // 测试 print 函数
    print(1, 2.0, "three", '4');
    
    return 0;
}

代码中的关键点:

  1. print 函数模板:

    • 接受一个固定参数 T firstArg 和一个变长参数包 Types... args
    • 使用 sizeof...(Types)sizeof...(args) 来获取剩余参数的数量。
  2. 递归调用:

    • 使用 if constexpr 来在编译时决定是否继续递归调用 print 函数。
    • 自 C++17 起,if constexpr 可以用于在编译时消除分支,以避免递归调用出现非法情况(即没有定义的 print() 调用)。
  3. 示例调用:

    • main 函数中调用 print 函数,并传入多种类型的参数进行测试。

这个示例展示了 sizeof... 的用法,以及如何利用数字来处理变参模板中的参数。

2.折叠表达式

C++17 引入的折叠表达式是一种用于处理变参模板参数包的语法糖,允许你使用二元运算符对参数包中的所有参数进行操作。本文的例子展示了如何计算参数包中所有参数的和:

template<typename… T>
auto foldSum (T… s) {
    return (+ s); // ((s1 + s2) + s3) …
}

在这个例子中,表达式 (… + s) 使用了折叠表达式。它会将展开的参数包 s 中的元素用 + 运算符逐个相加。比如,如果 s 是由元素 s1, s2, s3 组成,那么这个折叠表达式等同于 ((s1 + s2) + s3)

2.1 关于空参数包的处理

如果参数包 s 是空的,使用某些运算符的折叠表达式会导致不合规范的行为。例如:

  • 如果使用 + 运算符且参数包为空,编译器无法进行任何实际的运算,这样就会导致代码不合法。

但是某些运算符有特殊的行为或定义:

  • &&(逻辑与)运算符:

    • 当对一个空参数包使用时,折叠结果是 true。这是因为逻辑与对于空集合是恒真的。
  • ||(逻辑或)运算符:

    • 当对一个空参数包使用时,折叠结果是 false。这是因为逻辑或对于空集合是恒假的。
  • ,(逗号)运算符:

    • 对于一个空参数包,折叠结果是 void()。即没有任何有意义的运算发生,只是安全地处理了空参数包的情况。

这些默认行为为处理空参数包提供了一种一致且合理的方式,避免了因参数包为空导致的不确定性和错误。

2.1 完整代码示例

当然!下面这个例子展示了如何使用折叠表达式处理不同的运算符,包括对空参数包的处理。

#include <iostream>

// 计算参数包的和,如果参数包为空则编译会报错
template<typename... T>
auto foldSum(T... args) {
    return (... + args);
}

template<typename... Args>
auto multiply(Args... args) {
    return (... * args); // 将所有参数相乘
}

// 计算参数包的按位与,如果参数包为空则结果是所有位都是 1 的整数
template<typename... T>
auto foldAnd(T... args) {
    return (... & args);
}

// 处理逻辑与运算,空参数包的结果为 true
template<typename... T>
bool foldLogicalAnd(T... args) {
    return (... && args);
}

// 处理逻辑或运算,空参数包的结果为 false
template<typename... T>
bool foldLogicalOr(T... args) {
    return (... || args);
}

// 处理逗号运算符,空参数包的结果为 void()
template<typename... T>
void foldComma(T... args) {
    (..., (std::cout << args << " ", void()));
}

int main() {
    std::cout << "Sum: " << foldSum(1, 2, 3, 4) << std::endl;          // 输出:Sum: 10
    std::cout << "And: " << foldAnd(0b1111, 0b1101, 0b1011) << std::endl; // 输出:And: 9 (0b1001)

    // 逻辑与
    std::cout << std::boolalpha << "Logical And: " << foldLogicalAnd(true, true, false) << std::endl; // 输出:Logical And: false
    std::cout << "Logical And (empty): " << foldLogicalAnd() << std::endl; // 输出:Logical And (empty): true

    // 逻辑或
    std::cout << "Logical Or: " << foldLogicalOr(false, false, true) << std::endl; // 输出:Logical Or: true
    std::cout << "Logical Or (empty): " << foldLogicalOr() << std::endl; // 输出:Logical Or (empty): false

    // 逗号运算符的使用示例
    std::cout << "Comma operator output: ";
    foldComma(1, 2, 3, 4);  // 输出:1 2 3 4
    std::cout << "\nComma operator (empty): ";
    foldComma();  // 输出: (没有输出空格或其他内容)

    return 0;
}
解释:
  1. foldSum:计算参数包的总和,如果参数包为空,这个程序会有问题,因为一直应用 + 操作符需要至少一个数字。

  2. foldAnd:计算按位与。如果参数包为空,则由于每位默认都是 1,理论上结果是存储全为 1 的整数(需合法处理空情况)。

  3. foldLogicalAnd:使用逻辑与折叠,空参数包返回 true

  4. foldLogicalOr:使用逻辑或折叠,空参数包返回 false

  5. foldComma:打印参数包内容,如果参数包为空则不会输出任何东西。逗号运算符允许我们在不需要返回值的时候执行多个操作。

这些示例显示了处理参数包的不同策略,同时让你对不同折叠操作符的特殊行为有所了解。

2.2 折叠方式

概念

折叠表达式是C++17新引进的语法特性。使用折叠表达式可以简化对C++11中引入的参数包的处理,从而在某些情况下避免使用递归。折叠表达式共有四种语法形式。分别为一元的左折叠和右折叠,以及二元的左折叠和右折叠。

1、一元右折叠(unary right fold)
  ( pack op … )
  一元右折叠(E op …)展开之后变为 E1 op (… op (EN-1 op EN))
2、一元左折叠(unary left fold)
  ( … op pack )
  一元左折叠(… op E)展开之后变为 ((E1 op E2) op …) op EN
3、二元右折叠(binary right fold)
  ( pack op … op init )
  二元右折叠(E op … op I)展开之后变为 E1 op (… op (EN−1 op (EN op I)))
4、二元左折叠(binary left fold)
  ( init op … op pack )
二元左折叠(I op … op E)展开之后变为 (((I op E1) op E2) op …) op EN

![[Pasted image 20241007172335.png]]

什么是折叠表达式?

折叠表达式是一种用来处理可变数量参数(比如模板参数包)的方式。它能帮你把这些参数运算组合在一起。

op代表运算符:下列 32 个二元运算符之一:+ - * / % ^ & | = < > << >> += -= = /= %= ^= &= |= <<= >>= == != <= >= && || , . ->*。在二元折叠中,两个运算符必须相同。

pack代表参数包:含有未展开的形参包且在顶层不含优先级低于转型(正式而言,是 转型表达式)的运算符的表达式。

init代表初始值:不含未展开的形参包且在顶层不含优先级低于转型(正式而言,是 转型表达式)的运算符的表达式注意开闭括号也是折叠表达式的一部分。

这里的括号是必需的。但是,圆括号和省略号(…)不必用空格分隔。

初始值在右边的为右折叠,展开之后从右边开始折叠。而初始值在左边的为左折叠,展开之后从左边开始折叠。

不指定初始值的为一元折叠表达式,而指定初始值的为二元折叠表达式。

四种折叠方式

  1. 一元右折叠 (pack op ...)

    • 这是从右到左结合。
    • 展开为:E1 op (E2 op (... op EN))
    • 示例:(1 + 2 + 3) 变成 1 + (2 + 3)
  2. 一元左折叠 (... op pack)

    • 这是从左到右结合。
    • 展开为:(((E1 op E2) op ...) op EN)
    • 示例:(1 + 2 + 3) 变成 (1 + 2) + 3
  3. 二元右折叠 (pack op ... op init)

    • 这是从右到左结合,带初始值。
    • 展开为:E1 op (E2 op (... op (EN op init)))
    • 示例:(1 + 2 + 3 + 0) 变成 1 + (2 + (3 + 0))
  4. 二元左折叠 (init op ... op pack)

    • 这是从左到右结合,带初始值。
    • 展开为:((((init op E1) op E2) op ...) op EN)
    • 示例:(0 + 1 + 2 + 3) 变成 ((0 + 1) + 2) + 3

好的,让我们通过具体的例子进一步说明这些类型的折叠表达式的展开过程。我们将使用整数加法来说明这些模式。

假设我们有一组值 1, 2, 3, 4,我们将依次使用一元与二元折叠来展开这些表达式。

1. 一元右折叠 (E op ...)

表达式:(args + ...)

展开过程:

  • (1 + (2 + (3 + 4)))

这意味着最后一个元素先与倒数第二个元素结合,然后再与倒数第三个元素结合,依次类推,直到第一个元素。

2. 一元左折叠 (... op E)

表达式:(... + args)

展开过程:

  • (((1 + 2) + 3) + 4)

这意味着第一个元素与第二个元素先结合,然后结果再与第三个元素结合,依次类推,直到最后一个元素。

3. 二元右折叠 (E op ... op I)

表达式:(args + ... + 0)

假设初始值 I0

展开过程:

  • (1 + (2 + (3 + (4 + 0))))

与一元右折叠类似,只是在最后多了个初始值 0,这会变成一个含有初始值的右结合式。

4. 二元左折叠 (I op ... op E)

表达式:(0 + ... + args)

假设初始值 I0

展开过程:

  • ((((0 + 1) + 2) + 3) + 4)

这意味着初始值 0 先与第一个元素 1 结合,再与第二个元素结合,依次类推,直到最后一个元素。

这些不同的展开方式在一些非交换或非结合的操作中可能会影响结果,因此选择合适的折叠形式是很重要的。在加法中,因为加法是交换并结合的,所以无论选择哪种方式展开,结果都是相同的。

总结
  • 右折叠 从“最右边开始”,不断往左结合。
  • 左折叠 从“最左边开始”,不断往右结合。
  • 一元折叠 没有初始值,因此只涉及参数包中的元素。
  • 二元折叠 包含一个初始值,会用初始值和参数包中的元素一起结合。

希望这样能让折叠表达式更加通俗易懂!

加法例子

当然,下面是每种折叠表达式类型的具体例子。假设我们有一个变参模板函数,用于对一系列整数进行加法运算。我们使用加法运算符 + 来说明折叠表达式的用法:

  1. Unary Right Fold (( … op pack ))

    template<typename... Args>
    auto sum_right_fold(Args... args) {
        return (args + ...);
    }
    
    // 示例调用
    auto result = sum_right_fold(1, 2, 3, 4); // 结果为 1 + 2 + 3 + 4 = 10
    
  2. Unary Left Fold (( pack op … ))

    template<typename... Args>
    auto sum_left_fold(Args... args) {
        return (... + args);
    }
    
    // 示例调用
    auto result = sum_left_fold(1, 2, 3, 4); // 结果为 1 + 2 + 3 + 4 = 10
    
  3. Binary Right Fold (( init op … op pack ))

    template<typename... Args>
    auto sum_right_fold_with_init(Args... args) {
        return (0 + ... + args);
    }
    
    // 示例调用
    auto result = sum_right_fold_with_init(1, 2, 3, 4); // 结果为 0 + 1 + 2 + 3 + 4 = 10
    
  4. Binary Left Fold (( pack op … op init ))

    template<typename... Args>
    auto sum_left_fold_with_init(Args... args) {
        return (args + ... + 0);
    }
    
    // 示例调用
    auto result = sum_left_fold_with_init(1, 2, 3, 4); // 结果为 1 + 2 + 3 + 4 + 0 = 10
    

变参模板参数例子

//传统写法

//传统写法
int TestVT()
{
    return 1;
}

template<typename T,typename... Args>
int TestVT(T var0, Args... varn)
{ 
    std::cout << "args len is:" << sizeof...(varn) << std::endl;
    return var0 + TestVT(varn...);
}


int main()
{
    std::cout << "value is:"<< TestVT(1, 2, 3)<< std::endl;
    system("pause");

    return 0;
}


//变参写法


//变参写法
template <typename T>
int TestVT(T t) {
    std::cout << "cur value:" << t << std::endl;
    return t;
}

//c++17折叠表达式展开
template <typename... Args>
void TestVT(Args... args)
{
    //逗号表达式+初始化列表
    //int v[] = { (Add1(args),0)... };
 
    int d = (Add1(args)+ ...);
    std::cout << "cur sum:" << d << std::endl;
}


int  main()
{
    TestVT(1, 2,7);
    return 0;
}

运算符示例

当然!以下是完整的 C++ 代码示例,演示了如何使用折叠表达式来应用各种运算符。在这些示例中,我们定义了一些模板函数,它们使用折叠表达式来对可变参数进行操作。

#include <iostream>

// 加法
template<typename... Args>
auto sum(Args... args) {
    return (... + args);
}

// 减法
template<typename... Args>
auto subtract(Args... args) {
    return (... - args);
}

// 乘法
template<typename... Args>
auto multiply(Args... args) {
    return (... * args);
}

// 除法
template<typename... Args>
auto divide(Args... args) {
    return (... / args);
}

// 模运算
template<typename... Args>
auto mod(Args... args) {
    return (... % args);
}

// 按位异或
template<typename... Args>
auto bitwise_xor(Args... args) {
    return (... ^ args);
}

// 按位与
template<typename... Args>
auto bitwise_and(Args... args) {
    return (... & args);
}

// 按位或
template<typename... Args>
auto bitwise_or(Args... args) {
    return (... | args);
}

// 比较相等
template<typename... Args>
bool all_equal(Args... args) {
    return (... == args);
}

// 比较不等
template<typename... Args>
bool all_not_equal(Args... args) {
    return (... != args);
}

// 小于
template<typename... Args>
bool ascending(Args... args) {
    return (... < args);
}

// 大于
template<typename... Args>
bool descending(Args... args) {
    return (... > args);
}

// 左移
template<typename... Args>
auto left_shift(Args... args) {
    return (... << args);
}

// 右移
template<typename... Args>
auto right_shift(Args... args) {
    return (... >> args);
}

// 逻辑与
template<typename... Args>
bool all_true(Args... args) {
    return (args && ...);
}

// 逻辑或
template<typename... Args>
bool any_true(Args... args) {
    return (args || ...);
}

// 复合赋值运算符示例 (+=)
template<typename T, typename... Args>
void add_assign(T& init, Args... args) {
    ((init += args), ...);
}

int main() {
    std::cout << "Sum: " << sum(1, 2, 3, 4) << std::endl;
    std::cout << "Subtract: " << subtract(10, 1, 2, 3) << std::endl;
    std::cout << "Multiply: " << multiply(2, 3, 4) << std::endl;
    std::cout << "Divide: " << divide(100, 2, 5) << std::endl;
    std::cout << "Modulus: " << mod(100, 3, 2) << std::endl;
    std::cout << "Bitwise XOR: " << bitwise_xor(15, 5, 2) << std::endl;
    std::cout << "Bitwise AND: " << bitwise_and(15, 12, 10) << std::endl;
    std::cout << "Bitwise OR: " << bitwise_or(8, 4, 2) << std::endl;
    std::cout << "All Equal (1, 1, 1): " << std::boolalpha << all_equal(1, 1, 1) << std::endl;
    std::cout << "All Not Equal (1, 2, 3): " << std::boolalpha << all_not_equal(1, 2, 3) << std::endl;
    std::cout << "Ascending (1, 2, 3): " << std::boolalpha << ascending(1, 2, 3) << std::endl;
    std::cout << "Descending (3, 2, 1): " << std::boolalpha << descending(3, 2, 1) << std::endl;
    std::cout << "Left Shift (1 << 2 << 3): " << left_shift(1, 2, 3) << std::endl;
    std::cout << "Right Shift (8 >> 2 >> 1): " << right_shift(8, 2, 1) << std::endl;
    std::cout << "All True (true, true, false): " << std::boolalpha << all_true(true, true, false) << std::endl;
    std::cout << "Any True (false, false, true): " << std::boolalpha << any_true(false, false, true) << std::endl;

    int result = 0;
    add_assign(result, 1, 2, 3);
    std::cout << "Add Assign (0 += 1, 2, 3): " << result << std::endl;

    return 0;
}

每个运算符的结果。

 Sum: 10
Subtract: 4
Multiply: 24
Divide: 10
Modulus: 1
Bitwise XOR: 8
Bitwise AND: 8
Bitwise OR: 14
All Equal (1, 1, 1): true
All Not Equal (1, 2, 3): true
Ascending (1, 2, 3): true
Descending (3, 2, 1): false
Left Shift (1 << 2 << 3): 32
Right Shift (8 >> 2 >> 1): 1
All True (true, true, false): false
Any True (false, false, true): true
Add Assign (0 += 1, 2, 3): 6
结合 ->* 和折叠表达式
#include <iostream>
#include <string>

class MyClass {
public:
    int a;
    double b;
    std::string c;

    MyClass(int x, double y, std::string z) : a(x), b(y), c(z) {}
};

template<typename T, typename... Members>
void printMembers(T* objPtr, Members... members) {
    // 使用折叠表达式打印成员值
    (std::cout << ... << (objPtr->*members)) << std::endl;
}

int main() {
    MyClass obj(1, 3.14, "Hello");

    int MyClass::* ptrA = &MyClass::a;
    double MyClass::* ptrB = &MyClass::b;
    std::string MyClass::* ptrC = &MyClass::c;

    // 通过对象指针访问多个成员
    printMembers(&obj, ptrA, ptrB, ptrC);

    return 0;
}
结合 .* 和折叠表达式
#include <iostream>
#include <tuple>

class MyClass {
public:
    int x;
    double y;
    std::string z;

    MyClass(int a, double b, const std::string& c) : x(a), y(b), z(c) {}
};

template <typename T, typename... Members>
void printMembers(T&& obj, Members... members) {
    // 使用折叠表达式结合 .* 和 ->* 操作符来访问每个成员
    ((std::cout << (obj.*members) << " "), ...);
}

int main() {
    MyClass obj(1, 2.5, "example");

    // 定义指向 MyClass 成员的指针
    int MyClass::* ptrX = &MyClass::x;
    double MyClass::* ptrY = &MyClass::y;
    std::string MyClass::* ptrZ = &MyClass::z;

    // 使用 printMembers 通过对象 obj 访问每个成员
    printMembers(obj, ptrX, ptrY, ptrZ);

    return 0;
}
遍历二叉树的一条路径
#include <iostream>

// 定义二叉树节点结构
struct Node {
    int value;
    Node* left;
    Node* right;
    Node(int i = 0) : value(i), left(nullptr), right(nullptr) {}
};

// 定义左右子节点指针
auto left = &Node::left;
auto right = &Node::right;

// 使用折叠表达式遍历树
template<typename T, typename... TP>
Node* traverse(T np, TP... paths) {
    return (np ->* ...->*paths); // np ->* paths1 ->* paths2 ...
}

int main() {
    // 初始化更复杂的二叉树结构
    Node* root = new Node{ 0 };
    root->left = new Node{ 1 };
    root->right = new Node{ 3 };
    root->left->left = new Node{ 4 };
    root->left->right = new Node{ 2 };
    root->right->left = new Node{ 5 }; // 我们将找到这个节点

    // 有效路径:root -> right -> left
    Node* node = traverse(root, right, left);

    // 输出最终的节点值
    if (node) {
        std::cout << "Node value: " << node->value << std::endl;
    }
    else {
        std::cout << "Node not found." << std::endl;
    }

    // 清理内存
    delete root->right->left;
    delete root->left->right;
    delete root->left->left;
    delete root->right;
    delete root->left;
    delete root;

    return 0;
}

(np ->* ... ->* paths) 是C++17引入的折叠表达式的一种特殊用法,用来简化递归代码。这个表达式使用了折叠操作符和指向成员的指针操作符来遍历结构体或类的成员。

组成部分的解释
  1. np: 这是指向对象的指针(在这个例子中是 Node*),代表我们开始遍历的起点。

  2. ->* 操作符:

    • 这是C++中的一个操作符,用来通过指针访问类或结构的成员。
    • 左侧是对象指针,右侧是成员指针。
    • 例如,np ->* left 访问了 np 对象的 left 成员。
  3. ... (省略号):

    • 这是C++可变参数模板中的一部分,表示参数包展开。
    • 在折叠表达式中,省略号用来将操作应用于参数包中的所有元素。
  4. 折叠表达式 (np ->* ... ->* paths):

    • paths 是一个可变参数包,包含指向成员的指针(例如 &Node::left, &Node::right)。
    • 表达式的含义是:从 np 开始,依次通过 ->* 操作符访问 paths 中列出的每个成员,形成链式访问路径。
    • 等价于 (np ->* paths1) ->* paths2 ->* paths3 ...,直到用尽 paths 中的所有元素。
使用示例

在二叉树上下文中,如果你有一个路径表示为 left, right,并从根节点 root 开始,这个表达式会进行如下操作:

  • 首先,nproot,然后 np ->* left 会得到 root->left
  • 接着,继续 (root->left) ->* right,结果是 root->left->right

综上,(np ->* ... ->* paths) 能极大简化代码,因为它自动地将多个成员访问串联起来,适合用于链式的成员访问。


可变参输出
#include <iostream>

template<typename... Types>
void print(Types const&... args)
{
    // 使用逗号运算符和初始化列表来创建空格分隔
   
    (std::cout << ... << args) << ’\n’;
}

int main() {
    print(1, 2, 3, "hello", 4.5);
    return 0;
}
可变参输出加空格
#include <iostream>

// 定义类模板,用于为输出添加空格
template<typename T>
class AddSpace {
private:
    T const& ref; // 引用传入的参数

public:
    AddSpace(T const& r) : ref(r) {}

    // 重载输出运算符,在输出后添加空格
    friend std::ostream& operator<<(std::ostream& os, AddSpace<T> s) {
        return os << s.ref << ' '; // 输出传入的参数以及一个空格
    }
};

// 使用模板参数包打印所有参数,并在每个参数后添加空格
template<typename... Args>
void print(Args... args) {
    (std::cout << ... << AddSpace<Args>(args)) << '\n';
}

int main() {
    print(1, 2.5, "Hello", 'A');
    return 0;
}
代码说明
  1. AddSpace类模板:

    • 接受一个泛型类型 T,并持有对该类型对象的引用。
    • 在重载的 << 运算符中,对象输出后加上一个空格。
  2. print函数:

    • 是一个可变参数模板函数,接受任意数量的参数。
    • 使用了折叠表达式与AddSpace类模板实例化,将所有参数通过std::cout输出。
  3. 示例:

    • print(1, 2.5, "Hello", 'A'); 会输出:1 2.5 Hello A 在每个参数之间都有一个空格。

通过这样的设计,打印函数能够广泛适用于不同类型参数,并在参数之间自动插入空格,提升了代码的可读性和功能性。

注意在表达式 AddSpace(args)中使用了类模板的参数推导(见 2.9 节),相当于使用了AddSpace(args),它会给传进来的每一个参数创建一个引用了该参数的AddSpace 对象,当将这个对象用于输出的时候,会在其后面加一个空格。


3.变参模板在标准库中的使用

变参模板在泛型库的开发中具有重要作用,尤其是在C++标准库中。以下是变参模板的一些应用场景:

  1. 转发任意类型和数量的参数
    • 向一个由智能指针管理的对象的构造函数传递参数
      // create shared pointer to complex<float> initialized by 4.2 and 7.7
      auto sp = std::make_shared<std::complex<float>>(4.2, 7.7);
      
    • 向一个由库启动的线程传递参数
      std::thread t(foo, 42, "hello"); // call foo(42, "hello") in a separate thread
      
    • 向一个被push进vector中的对象的构造函数传递参数
      std::vector<Customer> v;
      v.emplace_back("Tim", "Jovi", 1962); // insert a Customer initialized by three arguments
      

通常情况下,变参模板使用移动语义实现参数的完美转发(perfect forwarding)。它们的声明方式如下:

namespace std {
    template<typename T, typename… Args>
    shared_ptr<T> make_shared(Args&&… args);

    class thread {
    public:
        template<typename F, typename… Args>
        explicit thread(F&& f, Args&&… args);
        // …
    };

    template<typename T, typename Allocator = allocator<T>>
    class vector {
    public:
        template<typename… Args>
        reference emplace_back(Args&&… args);
        // …
    };
}

注意,常规模板参数的规则同样适用于变参模板参数。例如:

  • 按值传递的参数会被拷贝,并且类型会退化(decay)

    template<typename… Args>
    void foo (Args… args); // args are copies with decayed types
    
  • 按引用传递的参数会是实参的引用,并且类型不会退化

    template<typename… Args>
    void bar (Args const&… args); // args are non-decayed references to passed objects
    

4.变参表达式

除了参数转发外,变参模板还可以用于其他操作,例如对参数进行计算。以下是相关示例:

  1. 计算参数的值

    • 将参数包中的所有参数翻倍后再传给 print()

      template<typename… T>
      void printDoubled(const T&… args) {
          print((args + args));
      }
      

      调用示例:

      printDoubled(7.5, std::string("hello"), std::complex<float>(4,2));
      

      这相当于:

      print(7.5 + 7.5, std::string("hello") + std::string("hello"), std::complex<float>(4,2) + std::complex<float>(4,2));
      
  2. 对每个参数加1

    • 注意省略号的位置
      template<typename… T>
      void addOne(const T&… args) {
          // print(args + 1…); // 错误:1… 被识别为小数
          print(args + 1); // 正确
          print((args + 1)); // 正确
      }
      
  3. 编译期计算与类型判断

    • 判断所有参数包中参数的类型是否相同

      template<typename T1, typename… TN>
      constexpr bool isHomogeneous(T1, TN…) {
          return (std::is_same<T1, TN>::value &&); // 从C++17开始支持
      }
      

      调用实例:

      isHomogeneous(43, -1, "hello"); // 扩展为:std::is_same<int,int>::value && std::is_same<int,char const*>::value,结果为 false
      
      isHomogeneous("hello", "", "world", "!"); // 结果为 true,因为所有的参数类型都被推断为 char const*
      

这种用法涉及折叠表达式(fold expression)的应用。此外,当按值传递时,类型退化(decay)也会发生,导致字符数组被推断为指向字符常量的指针(char const*)。


5.变参下标

在模板编程中,可以使用变参下标(Variadic Indices)来通过一系列下标访问集合中的元素。以下是相关示例:

  1. 通过变参下标访问集合元素

    • 函数模板的定义

      template<typename C, typename… Idx>
      void printElems(const C& coll, Idx… idx) {
          print(coll[idx]);
      }
      
    • 调用示例

      std::vector<std::string> coll = {"good", "times", "say", "bye"};
      printElems(coll, 2, 0, 3);
      

      该调用等价于:

      print(coll[2], coll[0], coll[3]);
      
  2. 使用非类型模板参数声明参数包

    • 函数模板的定义

      template<std::size_t… Idx, typename C>
      void printIdx(const C& coll) {
          print(coll[Idx]);
      }
      
    • 调用示例

      std::vector<std::string> coll = {"good", "times", "say", "bye"};
      printIdx<2, 0, 3>(coll);
      

      这个调用效果上与上一个示例是相同的。

通过这种方式,可以灵活地在模板中使用下标,以访问集合中的特定元素。无论使用带类型的变参函数参数还是使用非类型的模板参数,最终都能够达到按需访问集合中不同位置元素的效果。


6.变参类模板

在C++中,类模板可以是变参数的,这使得开发者能够创建高度灵活的模板结构。以下是一些变参类模板的示例和应用:

  1. 变参类模板定义

    • Tuple类模板

      template<typename… Elements>
      class Tuple;
      

      示例使用

      Tuple<int, std::string, char> t; 
      // 't' can hold an integer, a string, and a character
      

      这一部分将在第25章讨论。

  2. Variant类模板

    • Variant类模板

      template<typename… Types>
      class Variant;
      

      示例使用

      Variant<int, std::string, char> v; 
      // 'v' can hold either an integer, a string, or a character
      

      这一部分将在第26章介绍。

  3. Indices类模板

    • 定义一组下标的类型
      template<std::size_t…>
      struct Indices {};
      
  4. 通过Indices打印std::array或std::tuple的元素

    • 函数定义

      template<typename T, std::size_t… Idx>
      void printByIdx(T t, Indices<Idx…>) {
          print(std::get<Idx>(t));
      }
      
    • 示例使用

      std::array<std::string, 5> arr = {"Hello", "my", "new", "!", "World"};
      printByIdx(arr, Indices<0, 4, 3>());
      

      另一种使用方式:

      auto t = std::make_tuple(12, "monkeys", 2.0);
      printByIdx(t, Indices<0, 1, 2>());
      

这些示例展示了如何使用变参类模板来处理不同数量和类型的元素。这也是迈向元编程(meta-programming)的第一步,更多的信息将在第8.1节和第23章中介绍。

7.变参推断指引

在C++中,推断指引(也称为类模板推导指南)能够处理变参模板,使得模板实例化更加灵活。以下是关于变参推断指引的详细介绍:

变参推断指引

推断指引可以使用可变参数来匹配多个模板参数。以C++标准库中的std::array为例,有如下推断指引的定义:

namespace std {
    template<typename T, typename… U>
    array(T, U…) -> array<enable_if_t<(is_same_v<T, U> &&), T>, (1 + sizeof(U))>;
}
示例

假设我们使用如下初始化方式:

std::array a{42, 45, 77};

此时,推断指引的工作机制如下:

  • 类型推断
    • T 被推断为 array中的首元素的类型。
    • U... 被推断为剩余元素的类型。

所以,std::array中元素的总数目等于1 + sizeof...(U),这使得上述初始化等同于:

std::array<int, 3> a{42, 45, 77};
详细说明
  • std::enable_if<>使用
    • 这里使用的是一个折叠表达式来确保所有元素类型相同。
    • 等效展开为:
      is_same_v<T, U1> && is_same_v<T, U2> && is_same_v<T, U3> && ...
      
  • 如果结果是false(即数组中元素不是同一种类型),推断指引将被弃用,导致类型推断失败。

这样,标准库保证了在推断成功的情况下,数组中的所有元素类型是一致的。这种机制确保了类型安全和代码的正确性。

通过变参推断指引,C++提供了一种灵活而强大的模板推导机制,使开发者能够以简洁的方式处理复杂的类型推断场景。

8.变参基类

在C++中,变参模板和多重继承结合使用可以实现灵活的功能组合。以下是一个关于如何使用变参基类的例子。

变参基类及其使用

#include <string>
#include <unordered_set>

class Customer {
private:
    std::string name;

public:
    Customer(std::string const& n) : name(n) { }
    std::string getName() const { return name; }
};

struct CustomerEq {
    bool operator() (Customer const& c1, Customer const& c2) const {
        return c1.getName() == c2.getName();
    }
};

struct CustomerHash {
    std::size_t operator() (Customer const& c) const {
        return std::hash<std::string>()(c.getName());
    }
};

// 定义一个组合变参基类operator()的类
template<typename... Bases>
struct Overloader : Bases... {
    using Bases::operator()...; // C++17 允许
};

int main() {
    // 将 CustomerHash 和 CustomerEq 的功能组合到一个类型中
    using CustomerOP = Overloader<CustomerHash, CustomerEq>;

    // 利用不同的比较和 hash 函数创建 unordered_set
    std::unordered_set<Customer, CustomerHash, CustomerEq> coll1;
    std::unordered_set<Customer, CustomerOP, CustomerOP> coll2;
    // ...
}

解释

  1. Customer 类

    • 用于存储客户的名称,提供一个方法来获取客户的名称。
  2. 功能对象

    • CustomerEq:用于比较两个Customer对象的名称是否相同。
    • CustomerHash:用于为Customer对象生成一个 hash 值。
  3. 变参基类 Overloader

    • 通过变参模板template<typename... Bases>定义。
    • 多重继承自传入的基类。
    • 使用 using Bases::operator()...; 来引入所有基类的 operator()
  4. 使用示例

    • 定义类型CustomerOP,它组合了CustomerHashCustomerEq的功能。
    • 创建两个std::unordered_set,分别使用不同的比较和hash策略。

通过这种方式,可以有效地将多个功能对象的行为组合到一个类中,使得代码更加模块化和可复用。这种技术在需要动态组合不同策略的场景中特别有用,比如定制化数据结构和算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁金金

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值