语法糖
这部分特性不改变语言核心逻辑,却能极大减少重复代码,让代码更简洁、可读性更高,是日常开发中最容易上手的特性。
if-else 初始化语句(If with initializer)
C++17允许在if/else语句的条件判断前,先初始化一个变量,变量的作用域仅限于if/else代码块内部,避免变量污染全局或外层作用域。
使用场景:需要临时变量参与条件判断(如指针判空、容器查找、锁的获取),且变量无需在判断后复用。
代码对比(C++11 and C++17)
// C++11 写法:变量作用域超出if,可能被误复用autoit=map.find("key");if(it!=map.end()){cout<<it->second<<endl;}// C++17 写法:变量it仅在if内部有效,更安全、简洁if(autoit=map.find("key");it!=map.end()){cout<<it->second<<endl;}类似地,switch语句也支持初始化:
switch(autoval=getValue();val){case1:break;case2:break;}结构化绑定(Structured Bindings)
这是C++17最受欢迎的特性之一,允许一次性从“聚合类型”(结构体、数组、std::pair、std::tuple等)中提取多个成员,无需逐个赋值,代码简洁度大幅提升。
支持的类型:
结构体/类(必须是聚合类型,即无私有成员、无构造函数等);
数组(固定大小);
std::pair、std::tuple(STL常用容器);
std::array(固定大小数组)。
代码示例
#include<iostream>#include<map>#include<tuple>// 1. 结构体绑定structPoint{intx;doubley;};// 2. 数组绑定intarr[]={10,20,30};// 3. tuple绑定std::tuple<std::string,int,double>getInfo(){return{"Alice",25,90.5};}intmain(){// 结构体绑定Point p={1,3.14};auto[x,y]=p;// 一次性提取x和y,类型自动推导cout<<x<<", "<<y<<endl;// 输出:1, 3.14// 数组绑定(需指定绑定个数,与数组大小一致)auto[a,b,c]=arr;cout<<a<<", "<<b<<", "<<c<<endl;// 输出:10, 20, 30// tuple绑定auto[name,age,score]=getInfo();cout<<name<<", "<<age<<", "<<score<<endl;// 输出:Alice, 25, 90.5// 结合map使用(map的迭代器返回pair)std::map<std::string,int>scoreMap={{"math",95},{"english",88}};for(auto[key,val]:scoreMap){// 遍历map更简洁cout<<key<<": "<<val<<endl;}return0;}注意点:结构化绑定默认是“值绑定”,若需修改原对象,需加引用(auto& [x,y] = p;);若原对象不可修改,加const(const auto& [x,y] = p;)。
折叠表达式(Fold Expressions)
C++11引入可变参数模板后,处理可变参数时需要写递归或借助辅助函数,代码繁琐。C++17的折叠表达式的可以简洁地处理可变参数模板,支持算术运算、逻辑运算、函数调用等场景。
核心语法:将可变参数包与运算符结合,自动展开参数包,无需递归。
代码示例(常用场景)
#include<iostream>// 1. 求和(二元折叠:参数包与+结合)template<typename...Args>autosum(Args&&...args){return(args+...);// 折叠表达式,等价于 args1 + args2 + ... + argsN}// 2. 打印所有参数(函数调用折叠)template<typename...Args>voidprint(Args&&...args){(std::cout<<...<<args)<<endl;// 展开为 cout << args1 << args2 << ...}// 3. 逻辑与(判断所有参数是否都为true)template<typename...Args>boolallTrue(Args&&...args){return(args&&...);// 展开为 args1 && args2 && ...}intmain(){cout<<sum(1,2,3,4)<<endl;// 输出:10print("Hello"," ","C++17","!",123);// 输出:Hello C++17!123cout<<allTrue(true,1,3.14)<<endl;// 输出:1(true)cout<<allTrue(true,0,3.14)<<endl;// 输出:0(false)return0;}折叠表达式支持的运算符包括:+、-、*、/、%、&、|、^、&&、||、, 等,几乎覆盖所有常用场景,是可变参数模板开发的“神器”。
STL容器扩展
C++17对STL进行了大量补充和优化,新增了实用容器、简化了容器操作,同时提升了部分操作的性能,是日常开发中使用频率最高的部分。
新增容器:std::optional、std::variant、std::any
这三个容器解决了C++长期存在的“空值处理”和“多类型存储”问题,替代了传统的“指针+判空”“union(有局限性)”,更安全、更易维护。
std::optional:可选值容器
作用:表示一个“可能存在、也可能不存在”的值,避免使用 nullptr 或魔法值(如-1、0)表示空,语义更清晰、更安全。
使用场景:函数返回值可能为空(如查找操作、解析操作)。
#include<iostream>#include<optional>#include<string>// 查找字符串中的某个字符,找到返回该字符,找不到返回空std::optional<char>findChar(conststd::string&str,charc){autopos=str.find(c);if(pos!=std::string::npos){returnstr[pos];// 返回有效值}returnstd::nullopt;// 返回空值}intmain(){autores1=findChar("C++17",'7');if(res1.has_value()){// 判断是否有值cout<<"找到字符:"<<res1.value()<<endl;// 输出:找到字符:7}autores2=findChar("C++17",'x');if(!res2){// 简化判断:optional可直接作为bool值cout<<"未找到字符"<<endl;// 输出:未找到字符}// 安全访问:无值时返回默认值cout<<res2.value_or('?')<<endl;// 输出:?return0;}std::variant:多类型容器(类型安全的union)
作用:表示一个“只能存储多种指定类型中的一种”的值,替代传统union(传统union不支持非POD类型,如std::string),且是类型安全的(会检查当前存储的类型)。
使用场景:需要存储不同类型但互斥的值(如配置项、解析结果)。
#include<iostream>#include<variant>#include<string>// 定义一个variant,只能存储int、double、string中的一种usingMyVariant=std::variant<int,double,std::string>;// 访问variant的辅助函数(使用std::visit)voidprintVariant(constMyVariant&v){std::visit([](auto&&val){cout<<"当前值:"<<val<<endl;},v);}intmain(){MyVariant v1=100;// 存储intMyVariant v2=3.14;// 存储doubleMyVariant v3="C++17";// 存储stringprintVariant(v1);// 输出:当前值:100printVariant(v2);// 输出:当前值:3.14printVariant(v3);// 输出:当前值:C++17// 检查当前存储的类型if(v1.index()==0){// 0:int,1:double,2:stringcout<<"v1 存储的是int类型"<<endl;}// 尝试获取指定类型的值(失败会抛异常)try{autoval=std::get<int>(v1);// 成功autoval2=std::get<double>(v1);// 失败,抛异常}catch(conststd::bad_variant_access&e){cout<<"类型错误:"<<e.what()<<endl;}return0;}std::any:任意类型容器
作用:表示一个“可以存储任意类型”的值,与variant的区别是:variant需要提前指定可能的类型,any无需指定,更灵活,但类型安全不如variant(需手动检查类型)。
使用场景:需要存储任意类型且无法提前确定类型的场景(如通用配置、脚本绑定)。
#include<iostream>#include<any>#include<string>intmain(){std::any a1=10;std::any a2=3.14f;std::any a3=std::string("Hello");// 检查类型并获取值if(a1.type()==typeid(int)){cout<<std::any_cast<int>(a1)<<endl;// 输出:10}// 错误示例:类型不匹配,会抛异常try{std::any_cast<double>(a1);}catch(conststd::bad_any_cast&e){cout<<"类型转换失败:"<<e.what()<<endl;}// 重置any(清空值)a1.reset();if(!a1.has_value()){cout<<"a1 为空"<<endl;}return0;}STL容器新增实用方法
C++17为常用容器(std::map、std::unordered_map、std::vector等)新增了多个便捷方法,减少冗余代码,提升效率。
(1)std::map/unordered_map:try_emplace、insert_or_assign
解决了传统insert方法“插入时需先判断是否存在”的繁琐问题,同时避免不必要的拷贝。
#include<iostream>#include<map>#include<string>intmain(){std::map<std::string,int>scoreMap;// 1. try_emplace:不存在则插入,存在则不操作(避免拷贝)// 第一个参数是key,后面是value的构造参数scoreMap.try_emplace("math",95);// 插入成功scoreMap.try_emplace("math",100);// 已存在,不操作// 2. insert_or_assign:不存在则插入,存在则修改valuescoreMap.insert_or_assign("english",88);// 插入scoreMap.insert_or_assign("math",98);// 已存在,修改为98for(auto[k,v]:scoreMap){cout<<k<<": "<<v<<endl;// 输出:math:98, english:88}return0;}(2)std::vector:data() 方法增强、emplace_back 优化
C++17中,std::vector::data() 支持空vector(返回nullptr),同时emplace_back 进一步优化,支持直接构造对象,避免拷贝和移动(仅当参数为右值时)。
(3)std::string:string_view 配合使用
C++17新增 std::string_view(非容器,是字符串的“视图”),可以快速访问字符串,无需拷贝,配合std::string使用,提升性能(后续单独补充)。
std::string_view:轻量级字符串视图
核心作用:表示一个字符串的“只读视图”,不拥有字符串所有权,仅存储字符串的起始地址和长度,无拷贝、无内存分配,效率极高。
使用场景:函数参数传递、字符串截取、只读访问字符串(避免不必要的拷贝)。
#include<iostream>#include<string_view>#include<string>// 函数参数用string_view,避免拷贝voidprintString(std::string_view sv){cout<<sv<<", 长度:"<<sv.size()<<endl;}intmain(){std::string str="C++17 New Features";// 1. 直接用string构造string_view(无拷贝)std::string_view sv1=str;printString(sv1);// 输出:C++17 New Features, 长度:18// 2. 截取字符串(无拷贝)std::string_view sv2=sv1.substr(0,4);// 截取前4个字符printString(sv2);// 输出:C++17, 长度:4// 3. 直接用字符串字面量构造(无拷贝)printString("Hello C++17");// 输出:Hello C++17, 长度:11// 注意:string_view不拥有所有权,若原字符串被销毁,sv会变成野指针std::string_view sv3;{std::string temp="temp string";sv3=temp;}// sv3此时指向已销毁的内存,访问会未定义行为!return0;}注意点:string_view 不拥有字符串所有权,必须确保原字符串的生命周期长于string_view,否则会出现野指针。
std::filesystem:C++17 标准文件系统库
C++17 正式引入了跨平台文件系统库std::filesystem,让文件、目录、路径操作变得简单安全,无需再编写平台相关的文件操作代码。
核心作用:跨平台文件/目录管理、路径拼接与解析、遍历目录、判断文件类型、获取文件信息等。
优势:类型安全、自动处理路径分隔符/和\、无需第三方库、全平台通用。
极简使用
#include<filesystem>namespacefs=std::filesystem;常用功能
- 判断文件/目录是否存在:
fs::exists("path") - 创建目录:
fs::create_directory("dir")、fs::create_directories("a/b/c") - 遍历目录:
for (auto& e : fs::directory_iterator(".")) - 路径拼接:
fs::path p = "dir"; p /= "file.txt" - 获取文件名、后缀、父路径:
p.filename()、p.extension()、p.parent_path()
简洁示例
fs::path p="test/demo.txt";if(fs::exists(p)){std::cout<<"文件名:"<<p.filename()<<'\n';std::cout<<"后缀:"<<p.extension()<<'\n';}注意:GCC 8 以下需链接库:-lstdc++fs,高版本 GCC/Clang/VS 直接支持 C++17 即可使用。
性能与安全优化
这部分特性虽然不常直接写在代码中,但能间接提升程序性能、减少内存泄漏和未定义行为,是C++17对语言底层的重要优化。
1. 类模板参数推导(Class Template Argument Deduction, CTAD)
C++11/14中,模板类的构造函数需要显式指定模板参数,C++17允许编译器根据构造函数的参数,自动推导模板参数,简化代码,同时避免手动指定参数出错。
#include<iostream>#include<vector>#include<pair>#include<tuple>intmain(){// C++11/14 写法:必须显式指定模板参数std::vector<int>v1={1,2,3};std::pair<int,std::string>p1(1,"hello");std::tuple<int,double,string>t1(10,3.14,"test");// C++17 写法:自动推导模板参数(CTAD)std::vector v2={1,2,3};// 推导为 std::vector<int>std::pairp2(1,"hello");// 推导为 std::pair<int, const char*>std::tuplet2(10,3.14,"test");// 推导为 std::tuple<int, double, const char*>return0;}几乎所有STL模板类(vector、pair、tuple、shared_ptr、unique_ptr等)都支持CTAD,极大简化了模板类的使用。
2. constexpr 增强
C++11引入constexpr,用于定义“编译期常量”,但限制较多(如函数只能有一个return语句)。C++17大幅放宽了constexpr的限制,允许在constexpr函数中使用循环、条件判断、局部变量等,让更多代码可以在编译期执行,提升程序运行效率。
#include<iostream>// C++17 中,constexpr函数可以有循环和条件判断constexprintfactorial(intn){intres=1;for(inti=1;i<=n;++i){// 循环res*=i;}returnres;}// 编译期计算阶乘(运行时无需计算)constexprintf5=factorial(5);// 编译期计算出 120constexprintf10=factorial(10);// 编译期计算出 3628800intmain(){cout<<f5<<", "<<f10<<endl;// 直接输出编译期计算结果return0;}优势:将计算从运行时转移到编译期,减少运行时开销,同时保证常量的正确性(编译期检查)。
3. 消除未定义行为:if constexpr
if constexpr 是“编译期条件判断”,与普通if的区别是:普通if的两个分支都会被编译,而if constexpr 只会编译满足条件的分支,避免了编译期的未定义行为(如访问不存在的成员)。
使用场景:模板编程中,根据模板参数的类型,执行不同的代码分支。
#include<iostream>#include<type_traits>// 模板函数:根据类型打印不同内容template<typenameT>voidprintType(constT&val){// 编译期判断T是否为int类型ifconstexpr(std::is_integral_v<T>){cout<<"整数类型:"<<val<<endl;}elseifconstexpr(std::is_floating_point_v<T>){cout<<"浮点类型:"<<val<<endl;}else{cout<<"其他类型"<<endl;}}intmain(){printType(10);// 输出:整数类型:10printType(3.14);// 输出:浮点类型:3.14printType("hello");// 输出:其他类型return0;}如果用普通if,即使某个分支在运行时不会执行,编译时也会检查该分支的语法正确性(如访问不存在的成员),而if constexpr 只会编译满足条件的分支,避免了这种问题。
其他常用特性
1. 嵌套命名空间(Nested Namespaces)
C++17简化了嵌套命名空间的写法,无需层层嵌套,代码更简洁。
// C++11/14 写法namespaceA{namespaceB{namespaceC{voidfunc(){}}}}// C++17 写法(等价)namespaceA::B::C{voidfunc(){}}// 使用时不变A::B::C::func();2. 废弃throw(),使用noexcept
C++11引入noexcept,表示函数不会抛出异常,C++17正式废弃了旧的throw()语法,统一使用noexcept(更简洁、更明确)。
// C++11 写法(废弃)voidfunc()throw(){}// C++17 写法(推荐)voidfunc()noexcept{}