各位未来的自己:当你翻开这篇笔记时,大概率正对着当初写的 test07 ()、test21 () 一脸懵 —— 咱当初为了省事儿,函数名起得跟 “密码本” 似的,注释更是惜字如金,连个 “这测啥” 都没写。现在我把这些 “加密代码” 解密,改成 “人话” 函数名,加了比代码还长的注释,还顺便吐槽了当初踩过的坑,主打一个 “以后复习不骂街,看一眼就知道咋回事”!

一、先总览:咱的代码里藏了哪些 C++17 新特性?

先列个 “特性清单”,省得你翻半天不知道重点,对着 test0X 发呆:

折叠表达式(参数包 “叠罗汉”)、

类模板参数推导(不用手动写模板类型)、

auto 非类型模板参数(模板参数能传具体值)、

constexpr if(编译期分支,不生成无用代码)、

嵌套命名空间(A::B::C 直接写,不用嵌套 namespace)、

if-init 语句(if 里声明变量,用完就丢)、

结构化绑定(拆 tuple/struct 像 “拆快递”)、

std::variant(百变盒子,只能存一种类型)、

std::optional(带空值的神奇盒子,避免返回 null)、

std::any(万能收纳盒,能装任何类型)、

std::apply(元组拆包神器)、

std::make_from_tuple(用元组造对象)、

std::string_view(字符串观察者,不拷贝)、

std::as_const(给变量套只读壳)、

std::charconv(字符串数值转换神器)、

std::filesystem(跨平台文件管家)。

下面逐个拆解,每个特性都配 “改名后代码 + 大白话注释 + 当初踩过的坑”,保证你看一遍就回忆起来。

二、逐个攻破:C++17 新特性详解(附改名代码)

1. 折叠表达式:参数包的 “叠罗汉”(原 test01→TestFoldExpression)

核心作用:

以前处理可变参数模板,得写递归函数 + 终止函数,麻烦得像 “搭积木拆积木”。折叠表达式是 “参数包压路机”,用(args &&...)这种简洁语法,一句话就能遍历所有参数,支持逻辑运算、算术运算等。

当初的坑:
  • 忘了折叠表达式的语法格式,把(args &&...)写成args &&...(少括号);
  • 用错运算符,比如想做加法却写成(args &&...)(逻辑与)。
改名后代码

cpp

运行

#include <iostream>
using namespace std;

// 折叠表达式:一句话判断所有参数是否为“真”(非0即真)
template<class...Args>
bool AllTrue(Args... args) {
    // 核心:(args &&...) 是“逻辑与折叠”,遍历所有args做&&运算
    // 比如AllTrue(1,2,3) → (1 && (2 && 3)) → true
    return (args &&...); 
}

// 原test01→改名:明确测试折叠表达式,再也不用猜test01是啥
void TestFoldExpression() {
    // 场景1:所有参数非0,返回true
    cout << "所有参数非0(1,2,3,10):" << AllTrue(1, 2, 3, 10, 1, 2, 66) << endl; // 输出1(true)
    
    // 场景2:有一个参数为0(false),返回false
    cout << "含0参数(true,true,false):" << AllTrue(true, true, false) << endl; // 输出0(false)

    // 拓展:加法折叠(C++17支持所有二元运算符)
    template<class...Args>
    auto Sum(Args... args) {
        return (args + ...); // 加法折叠:(1 + (2 + (3 + 4))) → 10
    }
    cout << "1+2+3+4的和:" << Sum(1,2,3,4) << endl; // 输出10
}
复习重点:
  • 折叠表达式必须用()包裹,格式:右折叠(表达式 运算符 ...) 或左折叠 (... 运算符 表达式)
  • 支持的运算符:+-*/&&||等所有二元运算符;
  • 不用写递归终止函数,编译器自动展开参数包,代码量减少一半。

2. 类模板参数推导(CTAD):不用写模板类型的 “懒人福利”(原 test02→TestClassTemplateDeduction)

核心作用:

以前用类模板,必须手动写ClassTest<int>(100,200),像 “填表格必写类型”;C++17 的 CTAD 能让编译器根据构造函数参数 “猜类型”,直接写ClassTest(100,200),省掉模板类型。

当初的坑:
  • 类模板没有匹配的构造函数,CTAD 会推导失败(比如构造函数参数类型不统一);
  • 自定义模板推导指南时,和构造函数逻辑冲突,导致编译报错。
改名后代码

cpp

运行

#include <iostream>
using namespace std;

// 定义类模板:需要两个相同类型的参数
template<class T>
class ClassTest {
public:
    // 构造函数:接收两个T类型参数(为CTAD提供推导依据)
    ClassTest(T a, T b) {
        cout << "类模板构造:参数类型是" << typeid(T).name() << endl;
    }
};

// 原test02→改名:明确测试类模板参数推导
void TestClassTemplateDeduction() {
    // 传统写法:必须写ClassTest<int>,手动指定模板类型
    ClassTest<int> ct_old(100, 200); // 输出:参数类型是int

    // C++17新写法:CTAD自动推导模板类型(根据100和200是int,推导出T=int)
    auto ct_new1 = new ClassTest(100, 200); // 输出:参数类型是int
    auto ct_new2 = ClassTest(3.14, 5.67);  // 推导T=double,输出:参数类型是double

    // 当初的坑:如果构造函数参数类型不同,CTAD会推导失败!
    // ClassTest ct_err(100, 3.14); // 报错:无法推导T(int和double不统一)
    delete ct_new1;
}
复习重点:
  • CTAD 依赖构造函数参数推导,参数类型必须能统一到一种模板类型;
  • 若想支持 “不同类型参数推导”,可自定义deduction guide(推导指南);
  • 标准库容器(vector、map 等)已支持 CTAD,比如vector v={1,2,3}(不用写 vector<int>)。

3. auto 非类型模板参数:模板参数能传 “具体值”(原 test03→TestAutoNonTypeTemplate)

核心作用:

以前非类型模板参数只能传 “编译期常量”(如template<int N>),像 “只能填固定数字”;C++17 允许用auto作非类型模板参数,能自动推导参数的 “值类型”,支持 int、char、指针等编译期值。

当初的坑:
  • auto非类型模板参数当成 “类型参数”,传func1<int>()(错误!只能传值,不能传类型);
  • 传的 “值” 不是编译期常量(如func1<x>,x 是运行时变量),导致编译报错。
改名后代码

cpp

运行

#include <iostream>
using namespace std;

// 定义auto非类型模板参数:T会自动推导“值的类型”(如100→int,'a'→char)
template<auto T>
void PrintValue() {
    cout << "值:" << T << ",类型:" << typeid(T).name() << endl;
}

// 原test03→改名:明确测试auto非类型模板参数
void TestAutoNonTypeTemplate() {
    // 场景1:传int值,T推导为int
    PrintValue<100>(); // 输出:值:100,类型:int

    // 场景2:传char值,T推导为char
    PrintValue<'A'>(); // 输出:值:A,类型:char

    // 场景3:传bool值,T推导为bool
    PrintValue<true>(); // 输出:值:1,类型:bool

    // 当初的bug代码!错误示范:auto非类型模板参数不能传“类型”,只能传“值”
    // PrintValue<int>(); // 报错:int是类型,不是编译期值

    // 当初的坑:传运行时变量也会报错(必须是编译期常量)
    // int x = 200;
    // PrintValue<x>(); // 报错:x是运行时变量,不是编译期常量
}
复习重点:
  • auto非类型模板参数推导的是 “值的类型”,不是 “任意类型”;
  • 传的参数必须是 “编译期常量表达式”(如字面量、constexpr 变量);
  • 常用场景:定义 “固定大小的容器”“编译期常量函数” 等。

4. constexpr if:编译期 “按需生成代码”(原 test04→TestConstexprIfBranch)

核心作用:

以前写模板分支,不管条件是否成立,所有分支都会生成代码,像 “不管用不用,先把所有工具都带上”;C++17 的constexpr if能在编译期判断条件,只生成成立分支的代码,不生成无用代码,减少二进制体积。

当初的坑:
  • 忘了constexpr关键字,写成if (ok == true)(变成运行时分支,没意义);
  • 分支里有语法错误(即使条件不成立),编译也会报错(因为编译器会检查所有分支的语法)。
改名后代码

cpp

运行

#include <iostream>
using namespace std;

// 模板函数:根据ok的值,编译期决定生成哪段代码
template<bool ok>
constexpr void PrintStatus() {
    // 核心:constexpr if在编译期判断,只生成成立的分支代码
    if constexpr (ok == true) {
        // 当ok=true时,这段代码会生成,else分支不会生成
        cout << "状态:ok(编译期生成这段代码)" << endl;
    } else {
        // 当ok=false时,这段代码会生成,if分支不会生成
        cout << "状态:not ok(编译期生成这段代码)" << endl;
    }
}

// 原test04→改名:明确测试constexpr if
void TestConstexprIfBranch() {
    // 场景1:ok=true,编译期生成if分支代码
    PrintStatus<true>();  // 输出:状态:ok(编译期生成这段代码)

    // 场景2:ok=false,编译期生成else分支代码
    PrintStatus<false>(); // 输出:状态:not ok(编译期生成这段代码)

    // 当初的坑:即使分支不成立,语法错误也会导致编译失败!
    // if constexpr (ok == true) {
    //     int a = "hello"; // 语法错误,即使ok=false,编译也会报错
    // }
}
复习重点:
  • constexpr if的条件必须是 “编译期可计算的常量表达式”;
  • 不成立的分支会被 “丢弃”(不生成汇编代码),但语法必须正确;
  • 常用场景:模板特化、根据类型生成不同代码(如处理 int 和 string 的不同逻辑)。

5. 嵌套命名空间:不用 “套娃” 写 namespace(原 test07→TestNestedNamespace)

核心作用:

以前写多层命名空间,得嵌套namespace A { namespace B { namespace C {} } },像 “套娃” 一样麻烦;C++17 支持namespace A::B::C直接写,一行搞定多层命名空间,代码更简洁。

当初的坑:
  • 嵌套命名空间的顺序写反(如namespace B::Anamespace A::B是两个不同命名空间);
  • 在嵌套命名空间外调用函数时,忘了写完整命名空间路径(如只写C::func1(),没写A::B::C::func1())。
改名后代码

cpp

运行

#include <iostream>
#include <unordered_map>
using namespace std;

// C++17嵌套命名空间:一行搞定A::B::C,不用套娃
namespace A::B::C {
    void PrintNamespace() {
        cout << "调用A::B::C命名空间的函数" << endl;
    }
}

// 原test07→改名:明确测试嵌套命名空间和if-init语句
void TestNestedNamespaceAndIfInit() {
    // 场景1:测试嵌套命名空间调用
    A::B::C::PrintNamespace(); // 输出:调用A::B::C命名空间的函数

    // 场景2:测试if-init语句(C++17新特性)
    unordered_map<string, int> student_scores = { {"张三", 11}, {"王五", 21} };

    // 传统写法:先声明迭代器,再判断
    auto iter_old = student_scores.find("王五");
    if (iter_old != student_scores.end()) {
        cout << "传统写法:王五的分数:" << iter_old->second << endl; // 输出21
    }

    // C++17新写法:if里声明变量,变量只在if块内有效(用完就丢)
    if (auto iter_new = student_scores.find("王五"); iter_new != student_scores.end()) {
        cout << "if-init写法:王五的分数:" << iter_new->second << endl; // 输出21
    }
    // 当初的坑:if-init声明的变量,在if外面用不了!
    // cout << iter_new->second << endl; // 报错:iter_new未声明(只在if内可见)
}
复习重点:
  • 嵌套命名空间的顺序很重要(A::B::CA::C::B);
  • if-init 语句声明的变量,作用域仅限于if块(包括 else),避免变量污染;
  • if-init 常用于 “声明迭代器”“创建临时对象”,用完即销毁,代码更整洁。

6. 结构化绑定:拆 tuple/struct 像 “拆快递”(原 test05-test06→TestStructuredBinding)

核心作用:

以前想从 tuple 或 struct 里取多个值,得一个个用get<0>()obj.member,像 “拆快递一个个拿”;C++17 的结构化绑定能 “一次性拆包”,把 tuple/struct 的成员直接绑定到变量,代码更简洁。

当初的坑:
  • 绑定的变量数量和 tuple/struct 的成员数量不匹配(如 tuple 有 3 个成员,只绑 2 个变量);
  • 绑定 const 对象时,忘了加const修饰,导致想改值却报错。
改名后代码

cpp

运行

#include <iostream>
#include <tuple>
#include <string>
using namespace std;

// 定义结构体(用于测试结构化绑定)
struct Student {
    string name;
    int age;
    string gender;
};

// 原test05-test06→改名:明确测试结构化绑定
void TestStructuredBinding() {
    // 场景1:拆tuple(传统写法vs结构化绑定)
    auto student_tuple = make_tuple(string("张三"), 19, string("男"));

    // 传统写法:用tie绑定,需要提前声明变量,麻烦
    string old_name;
    int old_age;
    string old_gender;
    tie(old_name, old_age, old_gender) = student_tuple;
    cout << "传统tie写法:" << old_name << " " << old_age << " " << old_gender << endl;

    // C++17新写法:结构化绑定,一次性拆包,不用提前声明
    auto [new_name, new_age, new_gender] = student_tuple;
    cout << "结构化绑定写法:" << new_name << " " << new_age << " " << new_gender << endl;

    // 场景2:拆struct(更直观)
    Student stu = { "李四", 20, "女" };
    auto [stu_name, stu_age, stu_gender] = stu;
    cout << "拆struct:" << stu_name << " " << stu_age << " " << stu_gender << endl;

    // 当初的坑:绑定数量必须和成员数量一致!
    // auto [name, age] = student_tuple; // 报错:tuple有3个成员,只绑2个变量
}
复习重点:
  • 结构化绑定支持 tuple、pair、struct、array(成员必须是公有的);
  • 绑定的变量是 “引用” 还是 “值”,取决于原对象(原对象是 const,绑定变量也是 const);
  • 想修改绑定的变量,原对象必须是非 const(或用auto& [x,y]绑定引用)。

7. std::variant:“百变盒子”(原 test14→TestVariantBasic)

核心作用:

以前想让一个变量 “有时存 int,有时存 string”,得用 union(不支持非 POD 类型)或自定义类;C++17 的variant是 “类型安全的 union”,能存多种类型,但同一时间只能存一种,还能检查当前类型,避免访问错误。

当初的坑:
  • get<类型>()获取值时,类型和当前存储类型不匹配,抛出bad_variant_access异常;
  • 忘了variant默认初始化第一个类型(如variant<int, double>默认存 0),导致误以为是空的。
改名后代码

cpp

运行

#include <iostream>
#include <variant>
#include <string>
using namespace std;

// 原test14→改名:明确测试std::variant
void TestVariantBasic() {
    // 定义variant:能存int、double、string三种类型,同一时间只能存一种
    variant<int, double, string> var_box;

    // 场景1:给variant赋值,自动切换类型
    var_box = "hello C++17"; // 现在存string
    cout << "当前类型索引(string是第2个类型,索引从0开始):" << var_box.index() << endl; // 输出2

    var_box = 10; // 切换成int
    cout << "当前类型索引(int是第0个类型):" << var_box.index() << endl; // 输出0

    var_box = 3.14; // 切换成double
    cout << "当前类型索引(double是第1个类型):" << var_box.index() << endl; // 输出1

    // 场景2:检查当前存储的类型(用holds_alternative)
    var_box = "hello variant"; // 切回string
    cout << "是否存int?" << holds_alternative<int>(var_box) << endl;       // 输出0(否)
    cout << "是否存double?" << holds_alternative<double>(var_box) << endl; // 输出0(否)
    cout << "是否存string?" << holds_alternative<string>(var_box) << endl; // 输出1(是)

    // 场景3:获取variant的值(两种方式)
    // 方式1:用get<索引>()(索引要对应当前类型)
    cout << "用索引获取string:" << get<2>(var_box) << endl;

    // 方式2:用get<类型>()(类型要对应当前存储类型,否则抛异常)
    try {
        cout << "用类型获取string:" << get<string>(var_box) << endl;
        // 当初的坑:类型不匹配会抛异常!
        // cout << get<int>(var_box) << endl; // 抛出bad_variant_access
    } catch (const bad_variant_access& e) {
        cout << "获取失败:" << e.what() << endl;
    }
}
复习重点:
  • variant的类型列表中不能有重复类型(如variant<int, int>报错);
  • 默认初始化时,variant会默认构造第一个类型(如variant<int, string>默认存 0);
  • 获取值优先用get_if<类型>()(返回指针, nullptr 表示类型不匹配,不抛异常)。

8. std::optional:“带空值的神奇盒子”(原 test15→TestOptionalBasic)

核心作用:

以前函数想返回 “可能为空的值”(如查找失败返回 - 1),容易和 “正常值 - 1” 混淆;C++17 的optional是 “带空标记的盒子”,能明确表示 “有值” 或 “无值”,避免用特殊值(如 - 1、nullptr)表示空,更安全。

当初的坑:
  • 没检查optional是否有值,直接用*optopt.value(),无值时会抛异常;
  • opt.value_or(default)时,默认值的类型和optional的类型不匹配。
改名后代码

cpp

运行

#include <iostream>
#include <optional>
#include <string>
using namespace std;

// 原test15→改名:明确测试std::optional
void TestOptionalBasic() {
    // 场景1:optional的四种初始化方式
    optional<int> opt_empty;          // 无值(默认初始化,相当于nullopt)
    optional<int> opt_null = nullopt; // 显式初始化无值
    optional<int> opt_val = 10;       // 有值(存10)
    optional<int> opt_copy = opt_val; // 拷贝初始化(有值,存10)
    optional<string> opt_str = "hello"; // 存string类型

    // 场景2:检查是否有值(has_value())
    if (opt_empty.has_value()) {
        cout << "opt_empty有值:" << opt_empty.value() << endl;
    } else {
        cout << "opt_empty无值" << endl; // 输出这句
    }

    // 场景3:获取值的三种方式
    // 方式1:用*opt(无值时未定义行为,危险!)
    if (opt_val.has_value()) {
        cout << "用*获取opt_val:" << *opt_val << endl; // 输出10
    }

    // 方式2:用value()(无值时抛异常,安全)
    try {
        cout << "用value()获取opt_val:" << opt_val.value() << endl; // 输出10
        // cout << opt_empty.value() << endl; // 无值,抛出bad_optional_access
    } catch (const bad_optional_access& e) {
        cout << "获取失败:" << e.what() << endl;
    }

    // 方式3:用value_or(default)(无值时返回默认值,推荐!)
    cout << "opt_empty无值,返回默认值0:" << opt_empty.value_or(0) << endl; // 输出0
    cout << "opt_str有值,返回原值:" << opt_str.value_or("empty") << endl; // 输出hello

    // 当初的坑:用->访问成员时,必须确保有值!
    if (opt_str.has_value()) {
        cout << "opt_str的长度:" << opt_str->size() << endl; // 输出5("hello"的长度)
    }
}
复习重点:
  • optional不能存void(想表示 “无返回值的可选操作”,用std::optional<void>不合法,需用std::variant<monostate>);
  • 无值的optionalnullopt表示,不是nullptr
  • 推荐用has_value()检查值,用value_or()获取值(避免异常)。

9. std::any:“万能收纳盒”(原 test16→TestAnyBasic)

核心作用:

以前想存 “任意类型”,得用void*(不安全,容易类型错误);C++17 的any是 “类型安全的万能容器”,能存任何类型,但取的时候必须用对应的类型,否则抛异常,比void*安全得多。

当初的坑:
  • any_cast<类型>()取值时,类型和存储类型不匹配,抛出bad_any_cast异常;
  • 忘了anyreset()会清空值,后续调用has_value()会返回 false。
改名后代码

cpp

运行

#include <iostream>
#include <any>
#include <typeinfo>
using namespace std;

// 原test16→改名:明确测试std::any
void TestAnyBasic() {
    cout << boolalpha; // 让bool值输出true/false,不是1/0

    // 场景1:any的初始化和状态检查
    any any_empty; // 空any,无值
    cout << "any_empty是否有值?" << any_empty.has_value() << endl; // 输出false
    cout << "any_empty的类型名:" << any_empty.type().name() << endl; // 输出void(无类型)

    // 场景2:给any赋值(支持任意类型)
    any any_int = 1;                // 存int
    auto any_float = make_any<float>(5.0f); // 用make_any创建(更安全)
    any any_double(6.0);           // 存double

    // 检查赋值后的状态
    cout << "any_int是否有值?" << any_int.has_value() << endl; // true
    cout << "any_int的类型:" << any_int.type().name() << endl; // int
    cout << "any_float的类型:" << any_float.type().name() << endl; // float
    cout << "any_double的类型:" << any_double.type().name() << endl; // double

    // 场景3:修改any的值(两种方式)
    any_empty = 666; // 直接赋值,覆盖空值
    auto& new_float = any_float.emplace<float>(4.0f); // emplace直接构造,返回引用
    new_float = 4.5f; // 修改引用,any_float的值也会变
    cout << "any_float修改后:" << any_cast<float>(any_float) << endl; // 输出4.5

    // 场景4:清空any的值
    any_int.reset(); // 清空any_int的值
    cout << "any_int清空后是否有值?" << any_int.has_value() << endl; // false

    // 场景5:安全取值(用any_cast)
    try {
        // 类型匹配,成功取值
        auto val = any_cast<int>(any_empty);
        cout << "any_empty的值:" << val << endl; // 输出666

        // 当初的坑:类型不匹配,抛异常!
        auto wrong_val = any_cast<float>(any_empty); // 抛出bad_any_cast
    } catch (const bad_any_cast& e) {
        cout << "取值失败:" << e.what() << endl;
    }
}
复习重点:
  • any会存储值的 “完整类型信息”(包括 const、引用),取值时类型必须完全匹配;
  • 推荐用make_any<类型>(值)创建 any(比直接赋值更高效,避免临时对象);
  • 不要用 any 存储 “生命周期短的临时对象”(如局部变量的引用),会导致悬垂引用。

10. 其他必学特性(附改名代码 + 踩坑记录)

10.1 std::apply:元组拆包神器(原 test17→TestApplyUnpackTuple)

cpp

运行

#include <iostream>
#include <tuple>
#include <utility>
using namespace std;

// 普通函数:两数相加
int AddTwo(int a, int b) {
    return a + b;
}

// 原test17→改名:明确测试std::apply拆包
void TestApplyUnpackTuple() {
    // 场景1:用apply拆pair(pair是特殊的tuple)
    auto num_pair = make_pair(2, 3);
    // apply把pair的两个元素拆成AddTwo的参数(AddTwo(2,3))
    cout << "2+3=" << apply(AddTwo, num_pair) << endl; // 输出5

    // 场景2:用apply拆tuple,配合lambda
    auto add_lambda = [](auto a, auto b, auto c) {
        return a + b + c; // 三数相加
    };
    auto num_tuple = make_tuple(2, 3, 4);
    // apply把tuple的三个元素拆成lambda的参数(lambda(2,3,4))
    cout << "2+3+4=" << apply(add_lambda, num_tuple) << endl; // 输出9

    // 当初的坑:tuple的元素数量必须和函数参数数量一致!
    // apply(AddTwo, num_tuple) // 报错:AddTwo要2个参数,tuple有3个元素
}

复习重点apply的第一个参数是 “函数 / 函数对象”,第二个参数是 “tuple/pair”,元素数量必须和函数参数数量匹配。

10.2 std::make_from_tuple:用元组造对象(原 test18→TestMakeFromTupleCreateObj)

cpp

运行

#include <iostream>
#include <tuple>
#include <string>
using namespace std;

// 自定义类:需要name和age两个参数
class Student {
public:
    string name;
    int age;
public:
    Student(string n, int a) : name(n), age(a) {
        cout << "创建Student:" << name << " " << age << endl;
    }
};

// 原test18→改名:明确测试make_from_tuple
void TestMakeFromTupleCreateObj() {
    // 场景1:用tuple存构造函数参数
    auto student_args = make_tuple("张三", 19);
    // make_from_tuple把tuple的元素拆成Student的构造参数(Student("张三",19))
    Student stu1 = make_from_tuple<Student>(student_args);

    // 场景2:用move转移tuple的所有权(适合存字符串等可移动类型)
    Student stu2 = make_from_tuple<Student>(move(student_args));
    // 注意:move后student_args的string可能变成空(取决于类型的移动语义)

    // 当初的坑:tuple的元素类型和构造函数参数类型必须匹配!
    // auto wrong_args = make_tuple(19, "张三"); // 顺序错了
    // Student stu_err = make_from_tuple<Student>(wrong_args); // 报错:构造函数要(string,int)
}

复习重点make_from_tuple<类型>(tuple)的 tuple 元素类型、顺序,必须和 “类型” 的构造函数参数完全匹配。

10.3 std::string_view:字符串观察者(原 test19→TestStringViewObserve)

cpp

运行

#include <iostream>
#include <string>
#include <string_view>
using namespace std;

// 接收string_view的函数(不拷贝字符串)
void PrintStringView(string_view sv) {
    cout << "字符串视图:" << sv << endl;
}

// 原test19→改名:明确测试string_view
void TestStringViewObserve() {
    // 场景1:用C字符串初始化string_view(不拷贝)
    const char* c_str = "hello world";
    string_view sv1(c_str, strlen(c_str)); // 只存指针和长度,不拷贝
    cout << "sv1:" << sv1 << endl; // 输出hello world

    // 场景2:用string初始化string_view(不拷贝)
    string str = "hello C++17";
    string_view sv2 = str; // 指向str的内存,不拷贝
    cout << "sv2:" << sv2 << endl; // 输出hello C++17

    // 场景3:传递string_view给函数(无拷贝开销)
    PrintStringView(sv1); // 输出字符串视图:hello world

    // 当初的坑:string_view不管理内存,原字符串销毁后会悬垂!
    string temp_str = "临时字符串";
    string_view sv_temp = temp_str;
    temp_str.~string(); // 手动销毁temp_str(模拟离开作用域)
    // cout << sv_temp << endl; // 危险:sv_temp指向已销毁的内存
}

复习重点string_view是 “视图”,不拷贝、不管理内存,原字符串必须比它 “活得久”;适合作为函数参数(避免 string 拷贝)。

10.4 std::as_const:给变量套只读壳(原 test20→TestAsConstMakeConst)

cpp

运行

#include <iostream>
#include <string>
#include <type_traits>
using namespace std;

// 原test20→改名:明确测试std::as_const
void TestAsConstMakeConst() {
    string str = "C++17 as_const test";

    // 检查原字符串的const属性(0=非const)
    cout << "str是否为const?" << is_const<decltype(str)>::value << endl; // 输出0

    // 用as_const把str转为const引用(不是创建新对象!)
    const string& str_const_ref = as_const(str);
    // 当初的误解:以为as_const创建了新const对象,其实是返回const引用
    cout << "str_const_ref是否为const?" << is_const<decltype(str_const_ref)>::value << endl; // 输出1

    // 测试修改:原字符串仍可修改(as_const不改变原变量的const性)
    str = "修改后的字符串";
    cout << "修改后str_const_ref:" << str_const_ref << endl; // 输出修改后的值(因为是引用)

    // 当初的坑:不能修改as_const返回的引用!
    // str_const_ref = "不能修改"; // 报错:str_const_ref是const引用
}

复习重点as_const返回变量的const T&引用,不创建新对象;用于 “临时需要只读访问” 的场景,不改变原变量的属性。

10.5 std::filesystem:跨平台文件管家(原 test21→TestFilesystemBasic)

cpp

运行

#include <iostream>
#include <filesystem>
using namespace std;
namespace fs = std::filesystem; // 别名,简化代码

// 原test21→改名:明确测试filesystem
void TestFilesystemBasic() {
    // 场景1:定义路径(自动处理跨平台分隔符)
    auto test_dir = fs::path("./testdir"); // 相对路径

    // 检查目录是否存在
    if (!fs::exists(test_dir)) {
        cout << "目录testdir不存在" << endl;
        // 若不存在,创建目录(create_directory创建单个,create_directories创建多级)
        fs::create_directory(test_dir);
    } else {
        cout << "目录testdir已存在" << endl;
    }

    // 场景2:非递归遍历目录(只看当前层级)
    fs::directory_entry dir_entry(test_dir);
    cout << "非递归遍历" << dir_entry.path().filename() << ":" << endl;
    // directory_iterator遍历当前目录的文件/子目录
    for (const auto& entry : fs::directory_iterator(test_dir)) {
        if (entry.is_regular_file()) { // 是普通文件
            cout << "文件:" << entry.path().filename() << ",大小:" << entry.file_size() << "字节" << endl;
        } else if (entry.is_directory()) { // 是子目录
            cout << "子目录:" << entry.path().filename() << endl;
        }
    }

    // 场景3:递归遍历目录(包含所有子目录)
    cout << "\n递归遍历" << dir_entry.path().filename() << ":" << endl;
    // recursive_directory_iterator自动进入子目录
    for (const auto& entry : fs::recursive_directory_iterator(test_dir)) {
        if (entry.is_regular_file()) {
            cout << "文件:" << entry.path().filename() << ",大小:" << entry.file_size() << "字节,父目录:" << entry.path().parent_path() << endl;
        } else if (entry.is_directory()) {
            cout << "子目录:" << entry.path().filename() << endl;
        }
    }

    // 当初的坑:忘记加filesystem命名空间,或没包含<filesystem>头文件
    // 旧编译器可能需要链接库(如GCC加-lstdc++fs)
}

复习重点fs::path自动处理/\exists()检查路径存在,is_regular_file()/is_directory()判断类型;遍历用directory_iterator(非递归)和recursive_directory_iterator(递归)。

三、复习速查表(怕忘就看这个!)

特性 核心作用 关键语法 / 避坑点
折叠表达式 一句话展开参数包,支持算术 / 逻辑运算 1. 必须用()包裹(如(args + ...));2. 运算符要匹配参数类型;3. 不用写终止函数
类模板参数推导(CTAD) 自动推导模板类型,不用手动写<类型> 1. 构造函数参数类型要统一;2. 标准库容器(vector/map)已支持;3. 可自定义推导指南
auto 非类型模板参数 模板参数能传编译期值,自动推导值类型 1. 只能传编译期常量(如 100、'a');2. 不能传类型(如func<int>()错误);3. 推导的是值类型
constexpr if 编译期分支,只生成成立的代码 1. 条件必须是编译期常量;2. 不成立分支语法也要正确;3. 减少二进制体积
嵌套命名空间 一行写多层命名空间,不用套娃 1. 顺序重要(A::B::CA::C::B);2. 调用时写完整路径(A::B::C::func
结构化绑定 一次性拆 tuple/struct,不用逐个取成员 1. 绑定数量必须和成员数量一致;2. 支持 tuple/pair/struct/array;3. 变量作用域仅限当前块
std::variant 类型安全的 union,同一时间存一种类型 1. 类型列表无重复;2. 默认初始化第一个类型;3. 取值用get<类型/索引>(),不匹配抛异常
std::optional 带空值的盒子,避免用特殊值表示空 1. 无值用nullopt;2. 取值前用has_value()检查;3. 推荐value_or()避免异常
std::any 类型安全的万能容器,能存任何类型 1. 取值类型必须完全匹配;2. 用make_any创建更高效;3. 不管理内存,避免存临时对象
std::apply 元组拆包神器,把 tuple 元素拆成函数参数 1. 函数参数数量 = tuple 元素数量;2. 支持 pair(特殊 tuple);3. 配合 lambda 超灵活
std::string_view 字符串视图,不拷贝,轻量高效 1. 不管理内存,原字符串要活得久;2. 适合作为函数参数;3. 不支持修改字符串
std::as_const 返回变量的 const 引用,套只读壳 1. 不创建新对象,只是引用;2. 不改变原变量的 const 性;3. 用于临时只读访问
std::filesystem 跨平台文件操作,统一路径处理 1. fs::path自动处理分隔符;2. 遍历用directory_iterator;3. 检查存在用exists()

四、结尾:以后复习别再 “猜代码” 了!

这篇笔记把当初的test0X全改成了 “看得懂” 的函数名(比如TestVariantBasicTestFilesystemBasic),加了比代码还长的注释,还把踩过的坑(比如any_cast类型不匹配、string_view悬垂)标了出来。

C++17 新特性看着多,但核心都是 “让代码更简洁、更安全、更少踩坑”—— 比如折叠表达式少写递归,optional避免返回 - 1,filesystem跨平台不用改路径。记住这些 “偷懒又安全” 的原则,下次复习时,对着速查表看特性,对着代码看用法,再也不用对着 “密码式” 函数名发呆啦!

下面是我学习时的练习源代码,欢迎需要的同学交流学习。

头文件:

#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <charconv>
#include <variant>
#include <optional>
#include <any>
#include <tuple>
#include <string_view>
#include <utility>
#include <filesystem>
#include <ctime>
using namespace std;
using namespace std::filesystem;
inline int value=100;

/*
	函数声明
*/
void test01();
void test02();
void test03();
void test04();
void test05();
void test06();
void test07();
void test08();
void test09();
void test10();
void test11();
void test12();
void test13();
//std::variant(变体)练习
void test14();
//是否为空的神奇盒子 std::optional
void test15();
//any练习
void test16();
int add1(int a,int b);
//apply元祖拆包神器
void test17();
//make_from_tuple 使用元祖创建对象神器
void test18();
//string_view字符串观察者
void test19();
//将左值转化为const类型
void test20();
void test21();

源文件:

#include "折叠表达式.h"


int main() {

	//test01();
	//test02();
	//test03();
	//test04();
	//test05();
	//test06();
	//test07();
	//test08();
	//test09();
	//test10();
	//test11();
	//test12();
	//test13();
	//test14();
	//test15();
	//test16();
	//test17();
	//test18();
	//test19();
	//test20();
	test21();
	return 0;
}

void test21() {
	namespace fs = std::filesystem;
	auto testdir = fs::path("./testdir");
	//判断目录是否存在
	if (!fs::exists(testdir)) {
		cout << "file or director is not exists" << endl;
	}
	else {
		cout << "file or director is exists" << endl;

	}
	//遍历文件
	fs::directory_options opt(fs::directory_options::none);
	fs::directory_entry dir(testdir);
	//遍历当前目录
	cout << "show :\t" << dir.path().filename() << endl;
	for (fs::directory_entry const& entry : fs::directory_iterator(testdir, opt)) {
		if (entry.is_regular_file()) {
			cout << entry.path().filename() << "\t size:" << entry.file_size() << endl;
		}
		else if(entry.is_directory()) {
			cout << entry.path().filename() << "\t dir" << endl;

		}
	}
	cout << endl;

	//递归遍历所有文件
	cout << "show all:\t" << dir.path().filename() << endl;
	for (fs::directory_entry const& entry : fs::recursive_directory_iterator(testdir, opt)) {
		if (entry.is_regular_file()) {
			cout << entry.path().filename() << "\t size:" << entry.file_size() << "\t parent:"
				<< entry.path().parent_path() << endl;
		}
		else if (entry.is_directory()) {
			cout << entry.path().filename() << "\t dir" << endl;
		}
	}
		
}

//将左值转化为const类型
void test20() {
	string str = { "C++ as const test" };
	cout << is_const<decltype(str)>::value << endl;//0不是const属性

	const string str_const = as_const(str);
	cout << is_const<decltype(str_const)>::value << endl;//1 是const属性

}

void func1(string_view str_v) {
	cout << str_v << endl;

}
//string_view字符串观察者
void test19() {
	const char* charStr = "hello world";
	string str{ charStr };
	string_view str_v(charStr, strlen(charStr));

	cout << "str:" << str << endl;
	cout << "str_v:" << str_v << endl;
	func1(str_v);
}

class ClassTest6 {
public:
	string name;
	size_t age;

public:
	ClassTest6(string name,size_t age):name(name),age(age){
		cout << this->name << " " << this->age << endl;
	}
};
//make_from_tuple 使用元祖创建对象神器
void test18() {
	auto param = make_tuple("张三", 19);
	ClassTest6 ct6=make_from_tuple<ClassTest6>(param);
	ClassTest6 ct6_2=make_from_tuple<ClassTest6>(move(param));
}

int add1(int a, int b) {
	return a + b;
}
//apply元祖拆包神器
void test17() {
	//lambda表达式 匿名函数
	auto add_lambda = [](auto a, auto b, auto c) {
		return a + b + c;
		};

	cout << apply(add1, pair(2, 3)) << endl;
	cout << apply(add_lambda, tuple(2, 3, 4)) << endl;
}

//any练习
void test16() {
	cout << boolalpha << endl;
	any a;
	cout << a.has_value() << endl;
	cout << a.type().name() << endl;

	any b = 1;
	auto c = make_any<float>(5.0f);
	any d(6.0);
	cout << b.has_value() << endl;
	cout << b.type().name() << endl;

	cout << c.has_value() << endl;
	cout << c.type().name() << endl;

	cout << d.has_value() << endl;
	cout << d.type().name() << endl;

	//更改any的值
	a = 666;//直接重新赋值
	auto e = c.emplace<float>(4.0f);//e为新生成的对象引用

	//清空any的值
	b.reset();
	cout << b.has_value() << endl;
	cout << b.type().name() << endl;

	//使用any的值
	try {
		auto f= any_cast<int>(a);
		cout << f << endl;
	}
	catch (const bad_any_cast& e) {
		cout << e.what() << endl;
	}

	try {
		auto g = any_cast<float>(a);
		cout << g << endl;
	}catch(const bad_any_cast& e) {
		cout << e.what() << endl;
	}

}

//值是否为空的神奇盒子 std::optional
void test15() {
	optional<int> o1;//什么都不写默认初始化为nullopt
	optional<int> o2 = nullopt;//初始化为无值
	optional<int> o3 = 10;//用一个Tl类型的值来初始化
	optional<int> o4 = o3;//用另一个optional来初始化
	optional<string> o5 = "hello";

	//判断是否有值
	if (o1.has_value()) {
		cout << "o1 has value" << endl;
		cout << o1.value() << endl;
	}
	else {
		cout << "o1 doesn't have value" << endl;
	}

	if (o2.has_value()) {
		cout << "o2 has value" << endl;
		cout << *o2 << endl;
	}
	else {
		cout << "o2 doesn't have value" << endl;
	}


	if (o3.has_value()) {
		cout << "o3 has value" << endl;
		cout << o3.value() << endl;
	}
	else {
		cout << "o3 doesn't have value" << endl;
	}


	if (o4.has_value()) {
		cout << "o4 has value" << endl;
		cout << o4.value() << endl;;
	}
	else {
		cout << "o4 doesn't have value" << endl;
	}

	if (o5.has_value()) {
		cout << "o5 has value" << endl;
		cout << o5->c_str() << endl;;
		cout << o5.value() << endl;	//推荐使用
		cout << o5.value().c_str() << endl;
	}
	else {
		cout << "o4 doesn't have value" << endl;
	}

 }

//std::variant(变体)练习
void test14() {
	variant<int, double, string> d;//int 0 double 1 string 2
	d = "hello";
	cout << d.index() << endl;//2
	d = 10;
	cout << d.index() << endl; //0
	d = 3.14;
	cout << d.index() << endl; //1
	d = "hello";
	cout << d.index() << endl;//2

	//检查当前类型
	cout << holds_alternative<int>(d) <<endl;//0
	cout << holds_alternative<double>(d) << endl;//0
	cout << holds_alternative<string>(d) << endl;//1



}

//charconv的练习
void test13() {
	//整数字符串转换成整数
	string s1 = "123456789";
	int val = 0;
	
	auto res = from_chars(s1.data(), s1.data() + s1.size(), val);
	if (res.ec == errc()) {
		cout << "val:" << val << endl;
		cout << "distance:" << res.ptr - s1.data() << endl;
	}
	else if (res.ec == errc::invalid_argument) {
		cout << "invaild" << endl;
	}

	//小数字符串转小数
	s1 = "12.34";
	double val2 = 0;
	auto format = chars_format::general;
	res = from_chars(s1.data(), s1.data() + s1.size(), val2, format);
	cout << "val2:" << val2 << endl;
	cout << "distance:" << res.ptr - s1.data() << endl;

	//把整数转换成字符串
	s1 = "xxxxxxxxxxx";
	int val3 = 123456;
	auto res2 = to_chars(s1.data(), s1.data() + s1.size(), val3);
	cout << "str:" << s1 << endl;
	cout << "filled:" << res2.ptr - s1.data() << endl;

}

[[maybe_unused]] void func4() {
	cout << "test 江桂东非人哉" << endl;
}
void test12() {
	//func4();
}

_NODISCARD auto func3(int a, int b) {
	return a + b;
}
void test11() {
	int result=func3(1, 2);
	cout << result << endl;
}

void test10() {
	int i = 1;
	int result;
	switch (i) {
	case 0:
		result = 1;
		break;
	case 1:
		result = 2;
		//[[fallthrough]];
		__fallthrough;
	default:
		result = 0;
		break;
	}
	cout << result << endl;
}

void test09() {
#if __has_include("iostream")
	cout << "iostream exist" << endl;
#endif // __has_include("iosream")

#if __has_include(<iostream>)
	cout << "iostream exist" << endl;
#endif // __has_include("iosream")
}

//lambda表达式捕获*this
class ClassTest3 {
public:
	int num;

public:
	void func1() {
		//lambda表达式捕获*this
		auto lamfunc = [*this]() { 
				cout << this->num << endl;
			};
		lamfunc();
	}
};
void test08() {
	ClassTest3 a;
	a.num = 666;
	a.func1();

}

//c++17简单的嵌套命名空间
namespace A::B::C {
	void func1() {
		cout << "A::B::C的func1()" << endl;
	}
}


void test07() {


	//c++11
	unordered_map<string, int> stu1{ {"张三",11 }, { "王五",21 } };
	auto iter = stu1.find("王五");
	if (iter != stu1.end()) {
		cout << iter->second << endl;
	}

	//c++17
	if (auto iter = stu1.find("王五"); iter != stu1.end()) {
		cout << iter->second << endl;
	}

	A::B::C::func1();
}

void test06() {
	auto stu2 = make_tuple(string{ "张三" }, 20, string("man"));
	auto [name, age, gender] = stu2;
	cout << name << " "
		<< age << " "
		<< gender << endl;
}

void test05() {
	auto stu1 = make_tuple(string{"张三"},19,string("man"));
	string name;
	size_t age;
	//int age2;
	string gender;

	tie(name, age, gender) = stu1;
	cout << name << " "
		<< age << " "
		<< gender << endl;
}

class ClassTest2 {
	inline static int value2 = 200;
};

/*
	函数定义实现
*/

//编译期进行判断, constexpr if和else语句不生成代码
template<bool ok>
constexpr void func2() {
	if constexpr (ok == true) {
		//当0k=true,下面的else语句不生成汇编代码
		cout << "ok" << endl;
	}
	else {
		//当0k=false,上面的if语句不生成汇编代码
		cout << "not ok" << endl;
	}
}
void test04() {
	func2<true>();
	func2<false>();
}

//auto占位的非类型模版形参
template<auto T>
void func1() {
	cout << T << endl;
}
void test03() {
	func1<100>();
	//func1<int>(); bug代码
}

//类模版参数推导
template<class T>
class ClassTest {
public:
	//构造函数
	ClassTest(T, T) {

	}
};
void test02() {
	auto ct1 = new ClassTest(100, 200);
}

template<class...Args>
bool all(Args... args) {
	return(args &&...);
}

void test01() {
	cout << all(1, 2, 3, 10, 1, 2, 66) << endl;
	cout << all(true, true, false) << endl;
}


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐