【C++】可变参列表展开

1.

sizeof... 操作符获取参数包args中参数个数

sizeof...(args);

2. 参数包定义

  • typename...:用于声明一个参数包,表示一组类型。它通常出现在模板参数列表中。
  • typename... Args:声明了一个参数包,表示一组类型
  • Args... args:声明了一个参数包,表示一组值
template<typename... Args>
void function(Args... args);

3. 参数包展开

  • 参数包必须在编译期展开。运行时不可

  • 参数包展开是 C++11 引入的可变参数模板(Variadic Templates)的一个重要特性。它允许你对参数包中的每个参数依次执行某个操作。展开操作符 ... 用于指示编译器将参数包中的每个参数依次替换到表达式中,生成多个独立的表达式。

  • 总的来说:C++17以下使用递归或初始化列表法。C++17及以上使用折叠表达式或逗号表达式法。

以表达式 (std::cout << args << " ", 0)... 为例:
假设 args 是 {1, 2.5, "Hello", 'c', true},那么展开后会变成:

(std::cout << 1 << " ", 0),
(std::cout << 2.5 << " ", 0),
(std::cout << "Hello" << " ", 0),
(std::cout << 'c' << " ", 0),
(std::cout << true << " ", 0);
  • 在C++17之前,处理参数包通常需要递归模板函数。在现代 C++ 中,尽量避免使用递归展开,因为递归展开可能导致编译时间增加和调试信息复杂。优先使用折叠表达式或逗号展开,这些方法更高效且易于维护。

3.1 递归模板展开

  • 低于C++17时写法
  • 递归模板展开是通过递归调用来处理参数包中的每个元素。
  • 必须定义一个递归终止条件(如无参数的 print() 函数,或只有1个参数的print()),否则会导致无限递归。
    // 当参数包为空时
    void print() 
    {
        std::cout << std::endl;
    }
    
    /*
    //参数包只剩一个参数时,也可
    template<typename First>
    void print(First first)
    {
        std::cout << first <<std::endl;
    }
    */
    
    template<typename First, typename... Rest>
    void print(First first, Rest... rest) 
    {
        std::cout << first << " ";
        print(rest...); // 递归调用,处理剩余的参数
    }
    

3.2 折叠表达式展开

  • C++17起的折叠表达式。若能使用C++17及以上使用该法。

  • 折叠表达式可以简洁地展开参数包,并对每个参数执行相同的操作。

  • 折叠表达式的展开规则:
    左折叠 (args op ...) 的展开形式是:((a1opa2)opa3)op...opan
    右折叠 (... op args) 的展开形式是:a1op(a2op(a3op...opan))

  • 折叠表达式不仅支持算术运算符,还支持逻辑运算符、逗号运算符等。

//算术运算符
#include <iostream>

template<typename... Args>
auto sum(Args... args) {
    return (args + ...); // 左折叠
}

int main() {
    std::cout << sum(1, 2, 3, 4) << std::endl; // 输出:10
    return 0;
}
//逻辑运算符
template<typename... Args>
bool all(Args... args) {
    return (... && args); // 右折叠,所有参数都为 true
}

template<typename... Args>
bool any(Args... args) {
    return (... || args); // 右折叠,任意参数为 true
}

3.3 逗号表达式展开

将折叠展开作用于逗号操作符,即每个参数arg都会带入逗号表达式中。
逗号操作符(,)的折叠表达式,无论是左折叠还是右折叠,最终的展开效果是相同的,因为逗号操作符的特性使得每个表达式都会被依次执行,且只返回最后一个表达式的值。
每个arg都执行一次操作符,这里的操作符是逗号操作符。

template<typename... Args>
void print(Args... args) {
    (...,(std::cout << args << " ")); //右折叠
    ((std::cout << args << " "), ...); // 左折叠
}

3.4 初始化列表展开

  • 通过初始化列表和逗号表达式可以展开参数包,是在 C++11 没有折叠表达式 时,用 std::initializer_list 展开参数包的“黑魔法”写法
/*
① 在 C++ 中,逗号表达式 (a, b) 的结果是 b 的值,但会先执行 a。因此,(std::cout << args << " ", 0) 的结果是 0,但会先执行 std::cout << args << " "等语句。
   
② std::initializer_list<int>{(std::cout << args << " ", 0)...} 是一个初始化列表,它通过展开参数包 args... 来构造一个 std::initializer_list<int> 对象。每个参数 args 通过逗号表达式 (std::cout << args << " ", 0) 被处理,最终生成一个整数 0,并将其放入初始化列表中。所以初始化列表本身存储的值是展开次数个0。

③ void() 的作用是确保最外层逗号表达式表达式的结果是 void 类型。如果不加 void(),初始化列表的构造表达式会返回一个 std::initializer_list<int> 对象,这可能会导致编译器对返回值进行不必要的处理,甚至可能引发警告或错误。
*/

template <typename... Args>
void print(Args... args) 
{
    //这种逗号表达式后直接跟...的写法
    (std::initializer_list<int>{(
        std::cout << args << " ",
        std::cout.flush(),
        0)...}, void());
}
/*
  //也可,这是折叠表达式写法。但是没有必要,若C++17及以上,就不需要用初始化列表
  (std::initializer_list<int>{((
    std::cout << args << " ",
    std::cout.flush(),
    0),...)}, void());
*/

int main() {
    print(1, 2.5, "Hello", 'c', true);
    return 0;
}
posted @ 2025-08-01 10:46  仰望星河Leon  阅读(56)  评论(0)    收藏  举报