C++17 新特性学习笔记:从 “test0X 乱码“ 到 “一看就懂“ 的救赎指南
各位未来的自己:当你翻开这篇笔记时,大概率正对着当初写的 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::A和namespace 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::C≠A::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是否有值,直接用*opt或opt.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>);- 无值的
optional用nullopt表示,不是nullptr; - 推荐用
has_value()检查值,用value_or()获取值(避免异常)。
9. std::any:“万能收纳盒”(原 test16→TestAnyBasic)
核心作用:
以前想存 “任意类型”,得用void*(不安全,容易类型错误);C++17 的any是 “类型安全的万能容器”,能存任何类型,但取的时候必须用对应的类型,否则抛异常,比void*安全得多。
当初的坑:
- 用
any_cast<类型>()取值时,类型和存储类型不匹配,抛出bad_any_cast异常; - 忘了
any的reset()会清空值,后续调用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::C≠A::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全改成了 “看得懂” 的函数名(比如TestVariantBasic、TestFilesystemBasic),加了比代码还长的注释,还把踩过的坑(比如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;
}
更多推荐


所有评论(0)