C++17 详细特性解析(中)

🎯 折叠表达式

🤔 折叠表达式解决什么问题?

C++17 之前,当我们编写可变参数模板(Variadic Templates)时,通常需要使用递归来展开参数包。

就是按照下面的代码的书写形式:

cpp 复制代码
// C++11/14 的方式:递归模板
template<typename T>
T sum(T t) {
    return t;
}

template<typename T, typename... Args>
T sum(T first, Args... args) {
    return first + sum(args...); // 递归调用
}

void ShowList()
{
    // 编译器递归的终止条件,参数包是0个时,直接匹配这个函数
    cout << endl;
}

template <class T, class... Args>
void ShowList(T x, Args... args)
{
    cout << x << " ";
    // args是N个参数的参数包
    // 调用ShowList时,参数包的第一个传给x,剩下N-1传给第二个参数包
    ShowList(args...);
}

// 编译时递归推导解析参数
template <class... Args>
void Print(Args... args)
{
    ShowList(args...);
}

int main()
{
    auto total = sum(1, 2, 3, 4, 5);
    Print(1, string("xxx"), 2.2);
}

C++17 中折叠表达式就是为了优雅地解决这个问题而生的。它允许你使用简洁的语法直接对参数包中的所有参数进行二元操作符的 "折叠" 计算,有了折叠表达式上面的代码可以优化为如下:

cpp 复制代码
template<typename T, typename... Args>
T sum(T first, Args... args) {
    return (first + ... + args);
}

template <class... Args>
void Print(Args... args)
{
    (std::cout << ... << args) << '\n';
}

int main()
{
    cout << sum(1, 2, 3, 4, 5) << endl;
    Print(1, string("xxx"), 2.2);
}

📜 折叠表达式的语法

🔗 参考:https://en.cppreference.com/w/cpp/language/fold.html

折叠表达式(Fold Expressions)是 C++17 引入的一个强大特性 ,它简化了对参数包(parameter pack)的操作,使得处理可变参数模板时代码更加简洁高效。它允许你将**一个二元操作符(必须是二元操作符)**应用于一个参数包的所有参数上,将其 "折叠" 成一个单一的结果。它主要用在模板编程中,特别是可变参数模板的场景。

它的语法形式主要有四种(以二元操作符 op 为例):

a. 一元右折叠(pack op ...

  • 展开形式:(pack1 op (pack2 op (pack3 op ... (packN-1 op packN))))
  • 计算顺序:从右向左

b. 一元左折叠(... op pack

  • 展开形式:(((pack1 op pack2) op pack3) op ...) op packN)
  • 计算顺序:从左向右

c. 带初始值的二元右折叠(pack op ... op init

  • 展开形式:(pack1 op (pack2 op (pack3 op ... (packN op init))))
  • 计算顺序:从右向左

d. 带初始值的二元左折叠(init op ... op pack

  • 展开形式:(((init op pack1) op pack2) op ...) op packN)
  • 计算顺序:从左向右

💡 代码示例

cpp 复制代码
#include<iostream>

// 1、一元左折叠
template<typename... Args>
bool all(Args... args)
{
    return (... && args);
    // return ((true && true) && true) && false;
}

// 2、一元右折叠
template<typename... Args>
auto sum(Args... args) {
    return (args + ...);
    // 等价于 return (arg1 + (arg2 + (arg3 + ...)));
}

int main()
{
    std::cout << all(true, true, true, false) << std::endl; // 输出false
    std::cout << sum(1, 2, 3, 4);             // 输出 10
    return 0;
}

cpp 复制代码
#include<iostream>
#include<string>
#include<vector>

template<typename... Strings>
std::string concat_left(Strings... strs) {
    return (std::string("") + ... + strs);
    // 展开形式:(((std::string("") + str1) + str2) + str3);
}

template<typename... Strings>
std::string concat_right(Strings... strs) {
    return (strs + ... + std::string(""));
    // 展开形式:(str1 + (str2 + (str3 + std::string(""))));
}

// 一元左折叠
template<typename... Args>
void print(Args... args)
{
    (std::cout << ... << args) << '\n';
    // 等价于 (((std::cout << arg1) << arg2) << arg3) << ... << argN;
}

// 一个更高级的技巧,使用逗号运算符和lambda来添加分隔符
template<typename... Args>
void print_with_separator(Args&&... args) {
    auto print_elem = [](const auto& x) {
        std::cout << x << " ";
    };
    std::cout << "[";
    (..., print_elem(args)); // 一元左折叠
    // ((print_elem(arg1), print_elem(arg2)), print_elem(arg3))
    std::cout << "]" << std::endl;
}

template<typename T, typename... Args>
void push_back_vec(std::vector<T>& v, Args&&... args)
{
    (v.push_back(std::forward<Args>(args)), ...);
}

int main() {
    std::cout << concat_left("x", "y", "z") << "\n";
    std::cout << concat_right("x", "y", "z") << "\n";

    print(1, "hello", 3.14); // 输出 "1hello3.14"
    print_with_separator(1, "hello", 3.14); // 输出 "[1 hello 3.14 ]"

    std::vector<int> v;
    push_back_vec(v, 1, 2, 3);
    for (int x : v) {
        std::cout << x << " ";
    }
    std::cout << "\n";
    return 0;
}

逗号表达式 是实现多操作数批量执行的核心,它能将多个独立表达式用,连接成一个整体,规则是从左到右依次执行所有子表达式,最终整个逗号表达式的结果仅取最后一个子表达式的值 。代码中像(v.push_back(std::forward<Args>(args)), ...)(..., print_elem(args))这类写法,正是利用逗号表达式的特性,把参数包中每个元素对应的函数调用(push_back/print_elem)拼接成可被折叠表达式处理的单个操作,从而实现对参数包中所有元素的批量遍历执行,这也是 C++ 中用折叠表达式处理 "无返回值批量操作" 的经典技巧。

而且逗号表达式的执行顺序是语法级别的从左到右,完全不受括号嵌套的影响,哪怕折叠表达式是右折叠的形式,括号只会改变表达式的组合方式,不会改变逗号表达式子操作的执行顺序,所以无论用左折叠还是右折叠处理逗号表达式,参数包的处理顺序始终是从左到右。

cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    // 右折叠形式的逗号表达式:(a , (b , c))
    (cout << "1" << endl, (cout << "2" << endl, cout << "3" << endl));
    return 0;
}

运行结果 :依旧按1→2→3的顺序输出,而非括号嵌套的3→2→1,直接证明括号不影响逗号表达式的执行顺序。

核心执行段的反汇编如下(关键指令标注了对应操作),能清晰看到指令按1→2→3的顺序依次执行,无任何顺序反转:

bash 复制代码
main:
        push rbp
        mov  rbp, rsp
        ; 执行 cout << "1" << endl
        mov  esi, OFFSET FLAT:.LC0  ; 字符串"1"的地址
        mov  edi, OFFSET FLAT:_ZSt4cout ; cout对象地址
        call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; 输出"1"
        mov  rsi, rax
        mov  rdi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ ; endl
        call _ZNSolsEPFRSoS_E ; 输出换行
        ; 执行 cout << "2" << endl
        mov  esi, OFFSET FLAT:.LC1  ; 字符串"2"的地址
        mov  edi, OFFSET FLAT:_ZSt4cout
        call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
        mov  rsi, rax
        mov  rdi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
        call _ZNSolsEPFRSoS_E
        ; 执行 cout << "3" << endl
        mov  esi, OFFSET FLAT:.LC2  ; 字符串"3"的地址
        mov  edi, OFFSET FLAT:_ZSt4cout
        call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
        mov  rsi, rax
        mov  rdi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
        call _ZNSolsEPFRSoS_E
        ; 函数返回
        mov  eax, 0
        pop  rbp
        ret

还有一个比较好用的: 这个我们看一下样例:

cpp 复制代码
template<class First, class ...Rest>
void Print(First&& first, Rest&&... rest)
{
    std::cout << std::forward<First>(first); // 打印第一个参数,无前置空格
    // 仅对剩余参数折叠:每个参数前加空格,完美转发减少拷贝
    (..., (std::cout << " " << std::forward<Rest>(rest)));
    std::cout << std::endl;
}

所以其实我们可以认为是:不是就要一个 "..." 对 一个 " args ",而可以我们 " 封装这个 args "!


📌 特殊情况:参数包为空时

当参数包为空时,折叠表达式的行为取决于操作符和是否有初始值:

1️⃣ 一元折叠(没有初始值)

  • 如果对空包使用 &&,其值为 true
  • 如果对空包使用 ||,其值为 false
  • 如果对空包使用 ,,其值为 void()
  • 其他二元操作符对空包使用一元折叠会导致编译错误

2️⃣ 二元折叠(有初始值)

  • 如果参数包为空,表达式的结果就是初始值。这使得你可以处理空的参数包,让代码更健壮。
cpp 复制代码
template<typename... Args>
bool all(Args... args)
{
    return (... && args);
    // return ((true && true) && true) && false;
}

template<typename... Args>
auto sum(Args... args) {
    return (args + ...);
    // 等价于 return (arg1 + (arg2 + (arg3 + ...)));
}

int main()
{
    cout << sum() << endl; // 编译报错
    return 0;
}

🚀 为什么会引入折叠表达式?

C++17 引入折叠表达式,主要是为了简化可变参数模板中参数包的展开代码,避免了 C++11/14 中需要编写递归模板和终止函数的繁琐,让代码更简洁、可读性更高。编译器根据构造函数的参数自动推导模板参数类型,从而简化模板的实例化。

🎯 类模板参数自动推导 🚀

C++17 核心特性:类模板参数自动推导(CTAD,Class Template Argument Deduction) ,解决了 C++17 前实例化类模板必须显式指定模板参数的繁琐问题。编译器会根据构造函数传入的实参类型 / 数量,自动推导模板参数的具体类型,让模板使用和普通类一样简洁,大幅简化标准库容器、自定义类模板的实例化代码

仅需注意:必须显式指定模板参数的场景有三类:

无构造函数实参时编译器无推导依据,需显式指定;

需要精准控制模板参数类型、覆盖编译器默认推导结果时,需显式指定;

声明与变量初始化分离(先声明后赋值)时,编译器在声明阶段无法从后续初始化代码推导类型,同样必须手动显式调用类模板并指定参数。

cpp 复制代码
#include <iostream>
#include <vector>
#include <tuple>
#include <array>

// 示例1:标准库容器的自动推导
void containerDeduction() {
    // C++17之前需要显式指定模板参数,否则编译器无法推导
    std::vector<int> v1 = { 1, 2, 3 };

    // C++17支持自动推导:编译器从初始化列表/构造参数推导出模板参数为int
    std::vector v2 = { 4, 5, 6 };       // 推导为std::vector<int>
    std::vector v3{ "hello", "world" }; // 推导为std::vector<const char*>
    std::vector v4(10, 1);              // 从(整型, 整型)推导出std::vector<int>
    std::vector v5(v1.begin(), v1.end());// 从迭代器类型推导出std::vector<int>
    // std::vector v6;                 // 报错:无构造参数,编译器无法推导模板参数

    // 打印推导后的实际类型名(类型名格式随编译器不同略有差异)
    std::cout << typeid(v2).name() << std::endl;
    std::cout << typeid(v4).name() << std::endl;
    std::cout << typeid(v5).name() << std::endl;
    // std::cout << typeid(v6).name() << std::endl;

    // 该特性适用于绝大多数C++标准库模板容器/工具类
    std::array a = { 1, 2, 3 };          // 推导为std::array<int, 3>(同时推导类型和大小)
    std::pair p(1, 2.0);                 // 推导为std::pair<int, double>(分别推导两个参数类型)
    std::tuple t(1, 2.0, "three");       // 推导为std::tuple<int, double, const char*>

    // 验证推导后的模板类正常使用
    std::cout << "v2 size: " << v2.size() << std::endl;
    std::cout << "p first: " << p.first << ", second: " << p.second << std::endl;
}

int main() {
    containerDeduction();
    return 0;
}

🎯 非类型模板参数(C++17/20 增强)💡

非类型模板参数 :模板参数不是 "类型",而是具有固定类型的编译期常量值(如整型、指针、引用等)。

C++17 为其新增auto占位符特性,编译器可根据实例化时传入的编译期常量实参 ,自动推导非类型模板参数的具体类型,无需显式指定;C++20 进一步打破限制,允许浮点数、字面量类类型 (编译期可构造的简单结构体 /std::array等)作为非类型模板参数,大幅提升灵活性。

注意:C++20 前非类型模板参数仅支持「整型 / 枚举 / 指针 / 左值引用 /std::nullptr_t」,字符串字面量不能直接作为非类型模板参数(可通过数组名传递)。

cpp 复制代码
#include <iostream>
#include <string>
#include <string_view>
#include <array>
#include <typeinfo>

// 模板参数用auto占位,编译器自动推导非类型模板参数的具体类型
template<auto Value>
void printValue() {
    std::cout << "Value: " << Value << std::endl; // 打印常量值
    std::cout << "Type: " << typeid(Value).name() << std::endl; // 打印推导后的类型名
    std::cout << "---" << std::endl;
}

// 自定义字面量类类型(C++20支持):简单、编译期可构造的结构体,无复杂逻辑
struct Point {
    int x;
    int y;
    // 编译器自动生成constexpr构造函数,支持编译期初始化
};

// 可变参数非类型模板参数+auto,接收多个任意类型的编译期常量
template<auto... Values>
struct ValueList
{
    ValueList()
    {
        // 逗号折叠表达式:遍历所有非类型模板参数并打印
        ((std::cout << Values << ' '), ...);
        std::cout << std::endl;
    }
}; // 编译期常量值列表,运行期构造时打印所有值

const char arr[] = "hello"; // 全局字符数组,可作为非类型模板参数(指针类型)

int main() {
    printValue<42>();       // auto推导为int,Value是编译期常量42
    printValue<3.14>();     // auto推导为double(C++20开始支持浮点数)
    printValue<'A'>();      // auto推导为char,Value是字符常量'A'
    printValue<true>();     // auto推导为bool,Value是布尔常量true

    printValue<nullptr>();  // auto推导为std::nullptr_t
    // printValue<"hello">(); // 错误:字符串字面量不能直接作为非类型模板参数
    printValue<arr>();      // auto推导为const char*,传递数组名(首元素地址)

    // C++20特性:字面量类类型作为非类型模板参数
    printValue<Point{ 10, 20 }>(); // 推导为Point,编译期初始化{x:10,y:20}
    constexpr std::array arr_num{ 1, 2, 3, 4, 5 }; // 编译期数组
    printValue<arr_num>(); // 推导为std::array<int,5>,C++20支持

    // 实例化可变参数非类型模板,自动推导每个参数的类型
    ValueList<1, 2.5, 'a', "hello"> vl; // 依次推导int/double/char/const char*
    // printValue<"hello">(); // 报错:字符串字面量禁止作为非类型模板参数
}

🎯 嵌套命名空间定义(语法糖)📝

C++17 简化嵌套命名空间定义语法,是对传统嵌套命名空间的语法糖优化。

C++17 前定义多层嵌套命名空间,需要逐层嵌套namespace关键字和大括号,代码嵌套层级深、可读性差;

C++17 支持连续作用域解析符:: 直接定义多层嵌套命名空间,可一次性声明任意深度的嵌套命名空间,也支持 "混合写法"(部分层级用新语法,部分用传统语法),完全兼容旧代码,仅简化书写,功能无任何变化。

cpp 复制代码
#include <iostream>

// C++17之前的传统嵌套命名空间定义
// 缺点:每多一层命名空间,就多一层大括号嵌套,层级深时代码冗余
namespace A {
    namespace B {
        namespace C {
            void foo() {
                std::cout << "Old nested namespace style" << std::endl;
            }
        }
    }
}

// C++17的新语法:直接用::串联多层命名空间,一次性定义
// 等价于A::B::C,无需逐层嵌套,代码更简洁
namespace A::B::C {
    void bar() {
        std::cout << "New nested namespace style" << std::endl;
    }
}

// C++17混合写法:外层用新语法,内层用传统语法,完全兼容
namespace A::B {
    namespace D {
        void qux() {
            std::cout << "Partially nested namespace" << std::endl;
        }
    }
}

int main() {
    // 调用方式完全不变,和传统嵌套命名空间一致
    A::B::C::foo();
    A::B::C::bar();
    A::B::D::qux();
    return 0;
}

🎯 __has_include 预处理特性 🔍

C++17 引入__has_include预处理指令 ,用于编译期检查指定头文件是否可被包含 (存在且编译器可访问),是编写跨平台 / 跨编译器可移植代码的核心工具。

语法为__has_include(<系统头文件>)__has_include("自定义头文件")

返回编译期整型常量1表示头文件存在,0表示不存在;

常与#if/#elif/#else配合,实现头文件的条件包含、备选方案兼容(如标准库的实验版 / 正式版头文件),避免因头文件缺失导致的编译错误。

注意:__has_include预处理阶段执行的检查,并非运行期,不影响程序运行效率。

cpp 复制代码
#include <iostream>

// 示例1:检查C++17标准库头文件<optional>是否存在
#if __has_include(<optional>)
#   include <optional>          // 存在则包含,定义宏标记支持该头文件
#   define HAS_OPTIONAL 1
#else
#   define HAS_OPTIONAL 0       // 不存在则定义宏标记不支持
#endif

// 示例2:检查自定义头文件是否存在(项目内的本地头文件)
#if __has_include("my_header.h")
#   include "my_header.h"
#   define HAS_MY_HEADER 1
#else
#   define HAS_MY_HEADER 0
#endif

// 示例3:兼容不同编译器的文件系统库(正式版/实验版)
// 部分编译器仅支持experimental/filesystem,部分支持正式的filesystem
#if __has_include(<filesystem>)
#   include <filesystem>        // C++17正式版文件系统库
    namespace fs = std::filesystem; // 定义别名,统一调用方式
#elif __has_include(<experimental/filesystem>)
#   include <experimental/filesystem> // 实验版文件系统库
    namespace fs = std::experimental::filesystem; // 统一别名
#else
#   error "需要编译器支持C++17 filesystem库(正式版/实验版均可)" // 无支持则直接终止编译
#endif

int main() {
    // 打印头文件支持情况
    std::cout << "Optional support: " << HAS_OPTIONAL << std::endl;
    std::cout << "My header support: " << HAS_MY_HEADER << std::endl;

    // 条件使用支持的头文件功能,保证代码可移植
    #if HAS_OPTIONAL
        std::optional<int> opt = 42; // C++17可选值类型,避免空指针问题
        std::cout << "Optional value: " << opt.value() << std::endl;
    #else
        std::cout << "Optional not available" << std::endl;
    #endif
    return 0;
}
相关推荐
shehuiyuelaiyuehao2 小时前
String的杂七杂八方法
java·开发语言
开发者小天2 小时前
python返回随机数
开发语言·python
2 小时前
java关于时间类
java·开发语言
安全二次方security²2 小时前
CUDA C++编程指南(7.19&20)——C++语言扩展之Warp投票函数和Warp匹配函数
c++·人工智能·nvidia·cuda·投票函数·匹配函数·vote
lly2024062 小时前
C 标准库 - <stdlib.h>
开发语言
少控科技2 小时前
QT新手日记035
开发语言·qt
青川学长2 小时前
Cursor + Qt Creator 混合开发指南
开发语言·qt
嫂子开门我是_我哥2 小时前
第十五节:文件操作与数据持久化:让程序拥有“记忆”
开发语言·python
Trouvaille ~2 小时前
【Linux】进程信号(三):信号捕捉与操作系统运行原理
linux·运维·服务器·c++·操作系统·信号·中断