C++17(新特性)
目录
一 结构化绑定
二 inline 变量
三 if/switch 初始化语句
四 强制省略拷贝
五 if constexpr
六 折叠表达式
七 类模板参数自动推导
八 非类型模板参数自动推导
九 嵌套命名空间定义
十 __has_include
十一 optional
十二 variant
十三 any
十四 string_view
一 结构化绑定
- 结构化绑定是 C++ 的一个语法糖,允许从一个 数组,结构体,类,或者有多个数据成员对象中声明定义多个变量
auto [val1,val2,val3,....] = expression1. 参数必须和表达式中的值一样,不能少或者多。
2. 参数的声明周期随表达式的声明周期绑定。
3. 参数是表达式的拷贝或者引用。
4. 参数不能是类的私有对象,除非在类内部定义。
5. 参数不支持嵌套定义。
示例:
int arr[3] = { 1,2,3 }; auto& [x, y, z] = arr; // 拷贝捕捉 // auto [x, y, z,p] = arr; 必须和表达式参数一样 // auto [x, y] = arr; 必须和表达式参数一样// auto 会丢掉 顶层 const 和 引用 // 如果引用的是 const 引用,则会保留 const,否则权限放大 auto& [x, y, z] = arr; // 左值捕捉 auto&& [x, y, z] = arr; // 万能捕捉 const auto& [x, y, z] = arr; // 左值/右值捕捉// 用于迭代器 unordered_map<string, int> Map; for (auto& [first, second] : Map) {}class A { private:int a; public:int b;void fun(){// 类里可以绑定私有成员auto [x, y] = *this;} }; A a1; // auto [x, y] = a1; 不能绑定私有成员// auto [[x,y], z] = make_pair(A(), 1); 不能嵌套定义 auto [x,y] = make_pair(A(), 1);
二 inline 变量
在 .h 文件声明对象,在多个 cpp 重复包含会引起重定义,一般的做法是 static/const/extern 来改变对象的链接属性,即对象同名,地址不同。
示例:
Tree.h void fun(){} int a;Tree.cc #include "Tree.h" // fun() 重复定义 // a 重复定义Test.cc #include "Tree.h"---------------------------------------------Tree.h void fun(); extern int a;Tree.cc #include "Tree.h" void fun(){} // 声明定义分离,定义实现 int a = 10;Test.cc #include "Tree.h" // 只有 fun 的声明没实现 // 只有 a 的声明---------------------------------------------Tree.h static int a; const int b;Tree.cc #include "Tree.h" a = 10,b = 10;Test.cc #include "Tree.h" a = 100,b = 100;两个 cc 文件的 a,b 是同名变量,地址不同C++17 引入了 inline 变量,被 inline 修饰的变量在全局只有一份,不会像 const/static 生成同名的副本。
示例:
Tree.h static/const int a = 10; void fun();Tree.cc void fun(){ cout << &a << endl; }main.cc cout << &a << endl; fun();00A08B94 00A08B98-------------------------------------Tree.h inline int a = 10; void fun();Tree.cc void fun(){ cout << &a << endl; }main.cc cout << &a << endl; fun();0095B000 0095B000修饰类静态成员对象:
普通的静态成员对象必须在类外初始化,但如果是整型家族或是枚举常量这些,用 const 可在声明的时候定义,用 constexpr 可以定义任意类型的常量。
C++17 引入 inline 修饰类静态变量,可以直接就地初始化,针对任意类型
struct A {static const int a = 10;static const char c = 'c';// const static double a = 10; 只能修饰整型/枚举等inline static string str = "123"; // 任意初始化即可constexpr static double s = 1.1; // 必须用常量表达式初始化 };
三 if/switch 初始化语句
if 和 switch 的表达式可以像 for 循环一样 定义对象,判断结果。
示例:
string s = "hello world"; if (auto pos = s.find('h');pos != string::npos) { cout << s[pos] << endl; } else cout << "no what" << endl;switch (auto pos = s.find('h');pos) {case 0: {cout << s[0] << endl;break;}case 1: {cout << s[1] << endl;break;}default: {cout << "no what" << endl;} }
四 强制省略拷贝
在针对函数内传值返回,返回匿名对象/有名对象,都会生成临时对象放到寄存器里,然后用寄存器里的值初始化外部的对象,因为是传值返回,这 2 个行为都涉及拷贝构造(如果没有写移动构造),带来的开销比较大,如果 move 对象之后,一次移动 + 一次移动,也有开销,C++17规定,匿名对象直接优化成直接构造中间没有拷贝/移动的行为,有名对象的返回可能涉及移动构造。
示例:
struct A {A() { cout << "A()" << endl; }A(const A&) { cout << "A(const A&)" << endl; }A(A&&) { cout << "A&&" << endl; } };A fun() {A a1;return a1; }A val = fun();// A()--------------------------------A fun() {return A(); }A val = fun();// A()------------------------------A fun() {A a1;return move(a1); }A val = fun();// A() // A&&注意事项:
如果返回有名对象,编译器可能尝试调用移动构造进行优化,会检测有没有移动构造,如果强制删除,则会报错,如果是匿名对象则不会检测,删除也能跑通。
struct A {A() { cout << "A()" << endl; }A(const A&) { cout << "A(const A&)" << endl; }A(A&&) = delete; //{ cout << "A&&" << endl; } };A fun() {A a1;return a1; } // 无法引用 函数 "A::A(A &&)" (已声明 所在行数:3684) -- 它是已删除的函数// 匿名对象不用检测移动构造的逻辑 A fun() {return A(); }
五 if constexpr
常规的 if 判断是运行时的逻辑,且会生成所有分支的代码,参数可以是任意表达式,但会受模板限制。
if constexpr:编译时的逻辑,仅生成某个分支的代码,其他分支的代码丢掉,只能传入常量表达式,不受模板限制。
示例:
template <typename T> void fun(T val) {if(1) { cout << val.size() << endl; }else cout << val++ << endl; }string s; fun(s);// 常规的 if 会生成所有分支的代码, string 不能 ++,编译错误 void fun(T val) {if constexpr (1) { cout << val.size() << endl; }else cout << val++ << endl; }// if constexpr 会舍弃其他分支的代码, val++ 被舍弃调,编译通过另一个玩法,解决模板元编程 SFINAE 替换失败不是错误,尝试匹配其他的版本,定义多个函数模板来决定实例化对应的类型,用 if constexpr 不用写冗余函数。
示例:
template <typename T> auto fun(T val) -> decltype(val.size()) {cout << "string" << endl;return val.size(); }template <typename T> auto fun(T val) -> decltype(val++) {cout << "int" << endl;return val; }-----------------------------template <typename T> void fun(T val) {if constexpr (is_integral_v<T>) cout << "int" << endl;else cout << "string" << endl; }
六 折叠表达式
C++11 提供的参数包解包的时候需要用到空函数来决定终止条件。
示例:
void fun() {}template <class T, class ...Args> void fun(T first,Args... args) {cout << first << endl;fun(args...); }C++17 引入折叠表达式就不必再写冗余函数,直接在主函数内部展开所有逻辑。
必须使用二元运算符:+ * / % ^ & |= < > << >> += = *= /= %= ^= &= |= <<= >>= == != <= >= && || , . * ->*
左折叠:从左往右运算。
右折叠:从右往左运算。
1. 一元折叠表达式
一元左折叠
template <class ...Args> int fun(Args... args) {return (... + args); }一元右折叠m
template <class ...Args> int fun(Args... args) {return (args + ...); }2. 二元折叠的表达式
二元左折叠
template <class ...Args> int fun(Args... args) {return (val + ... + args); }二元右折叠
template <class ...Args> int fun(Args... args) {return (args + ... + val); }函数嵌套:
template <class T> T _fun(T val) {return val; }template <class ...Args> void fun(Args... args) {cout << ('p' + ... + (_fun(args))) << endl; }fun(string("1"), string("2"), string("3"), string("4"));// 二元左折叠注意事项:逗号表达式无论什么情况都是从左往右执行,不管是左折叠还是右折叠。
七 类模板参数自动推导
函数模板可以显示实例化也可以隐式编译器自动推导类型,类模板必须显示实例化,C++17 引入类模板参数自动推导,就不需要显示的写类型了。
示例:
tuple<int, string, double, char> tp1 = { 1,"1",1.1,'1' }; tuple tp2 = { 1,"1",1.1,'1' };vector v = { 1 };
八 非类型模板参数自动推导
在模板中,类型模板用 class/typename 修饰,非类型模板指定类型修饰,但写死了,C++17 引入 auto 可以自动推导非类型模板参数的类型,但不是所有类型支持。
示例:
template </* size_t N */ auto N> class A {}; const char s[] = "123";A<1> a1; A<1.1> a2; // C++ 20 支持 A<'a'> a3; A<false> a4; A<s> a5;做参数包:
template <auto... N> class A {};A<1, 'c', 1.1> a6;
九 嵌套命名空间定义
C++为了区分同名变量通过域作用限定符来访问不同的对象,也就是对象被封到命名空间李了,如果命名空间嵌套的太深了,太冗余了,C++ 17 给了一套新的定义方式。
示例:
// C++ 17 之前 namespace A {namespace B{namespace C{}} };// C++ 17 namespace A::B::C { int a = 10; }namespace xxx= A::B::C;A::B::C::a = 20; xxx::a = 100;
十 __has_include
__has_include(<头文件>):预处理阶段判断某个头文件在不在。
示例:
#if __has_include(<vector>)string s = "vector"; #elsestring s = "unknow"; #endif cout << s << endl;// vector
十一 optional
在调用某个接口,查找某个对象的时候找不到,string返回 string::npos,链表返回 nullptr,或者一些其他的处理结果,这些返回结果都不统一,可能返回的值就是想要的值,C++17 引入了 optional 来保存返回的结果是合法还是不合法的。
常见的接口:
value: 拿取里面的值,不存在结果未定义 value_or: 拿取里面的值,不存在返回 nullopt_t has_value/operator bool: 判断是否有值 reset: 释放里面的值示例:
optional<char> fun() {string s = "hello world";if (auto it = s.find('h');it != string::npos){return optional<char>(s[it]);}else return optional<char>(); }optional<char> op = fun();if (op.has_value()/* op */) {cout << op.value() << endl; } else cout << "nullopt" << endl;
十二 variant
C语言中的 union 联合体,所有成员对象共享最大的那个对象的空间,使用时容易难以避免错误,比如把存储的是 int ,访问 char 对象,行为的未定义的,C++17 引入了 variant<T...>,对联合体更细致的封装。
variant<T...>:能容纳任意多个类型,提供全局的多个访问接口,不合法则抛异常或其他行为。
常用接口:
index() : 返回实际存的值的下标 emplace<下标/类型> : 插入数据,指定下标/类型插入 get<下标/类型> : 根据下标/类型获取值,没有抛异常 get_if<下标/类型> : 根据下标/类型获取值,没有返回空 visit<可调用对象/vairant<T...>对象>: 根据传入的 vairant 对象编译时确定实际的存储的值,传给可调用对象示例:
variant<int, string> v = 1; cout << "int -> " << v.index() << endl; v = "hello world"; cout << "string -> " << v.index() << endl << endl;v.emplace<int>(10); v.emplace<0>(10);v = 1; int a1 = get<int>(v); // string str1 = get<string>(v); // str1 行为未定义int* a2 = get_if<int>(&v); string* str2 = get_if<string>(&v); // str2 为空 cout << "a2 ->" << a2 << " " << "*a2 ->" << *a2 << endl; cout << "str2 ->" << str2 << endl << endl;visit([](auto&& x){// 得到 x 的原始类型,decltype 会保留顶层 const 和 引用using type = decay_t<decltype(x)>;if constexpr (is_same_v<int, type>){cout << "int ->" << x << endl;}else if constexpr (is_same_v<int, type>) cout << "int ->" << x << endl;else cout << "unknow" << endl;}, v);------------------------------------------int -> 0 string -> 1a2 ->004FF738 *a2 ->1 str2 ->00000000int ->1
十三 any
上面说的 variant 针对知道想要什么类型,在这些类型之间自由切换,而 any 则是针对不知道要什么类型,可以接收任意类型而存在的。
常用接口:
emplace<T> : 显示插入指定类型对象 reset : 释放存储的对象 has_value : 判断是否有对象 type : 返回对象的类型 any_cast : 得到里面的对象示例:
any a1 = 1; a1.emplace<string>("123"); a1.emplace<int>(10);a1.reset(); if (a1.has_value()) {//如果类型不匹配则抛异常cout << any_cast<int>(a1) << endl; } else cout << "unknow" << endl;a1 = 1; const type_info& ti = a1.type(); cout << ti.name() << endl;---------------------------- unknow intvariant VS any
- variant 针对知道要什么类型,any 不知道要什么类型
- variant 的对象在自己内部,any 的小对象在自己内部,大对象用指针指向堆
- variant 提供 visit 编译时检测对应的类型值,any 要写 if else any_cast 运行时判断
十四 string_view
传统的 std::string ,可能涉及拷贝构造,移动构造等操作,会影响效率,C++ 17 引入了 string_view 来作为 string 的视图,也就是观察他,不修改它,和 weak_ptr 类型。
他的接口和 string 类型,没有修改接口。
string_view默认不包含 '\0'。
示例:
// 无参构造,底层指针为空,string 会多开辟一个空间存储 '\0' string_view sv1;// 常量字符串构造 string_view sv2("123");// string 构造 string s1 = "hello world"; string_view sv3(s1);cout << "sv3 -> " << sv3 << endl; cout << "sv3 -> "; for (auto& e : sv3) {// e = 'a'; 不可修改cout << e; } cout << endl;// 从左往右调整指针指向 sv3.remove_prefix(6);// 从右往左调整指针指向 // sv3.remove_suffix(6); cout << "sv3 -> " << sv3 << endl;// 取里面的指针 const char* p1 = sv3.data(); cout << "p1 -> " << *p1 << endl;// 截取子串,没有深拷贝 string_view sv4 = sv3.substr(0,5);if (auto it = sv4.find('w');it != string_view::npos) {cout << "sv4[it] -> " << sv4[it] << endl; }---------------------------sv3 -> hello world sv3 -> hello world sv3 -> world p1 -> w sv4[it] -> w注意事项:
- string_view 指向的是局部对象,局部对象销毁,string_view 则是未定义的
- string_view 不存储 '\0',使用一些 C 风格的API行为可能未定义
