C++17特性汇总 这里只挑取一些比较感兴趣的话题
结构化绑定Structured Binding
struct B { int a = 1 ; int b = 2 ; };struct D1 : B {};auto [x, y] = D1{}; struct D2 : B { int c = 3 ; };auto [i, j, k] = D2{};
int arr[] = { 47 , 11 };auto [x, y] = arr;
如果需要可写的,还需要再多定义几个返回值不是const限定的get。
内敛变量 首先:
C++不允许在类内对非常量静态变量进行初始化,这是因为如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。
C++可以在类定义的外部定义并初始化非常量静态成员,但注意如果被多个 CPP 文件同时包含的话又会引发新的错误,即违反ODR原则,预处理#pragma once
也没用,因为编译顺序还是会影响了这个静态变量的值最后是什么。
比较好的一种实践是: .h定义类并类声明类静态变量,在.cpp中进行定义,即一个翻译单元里才拥有它。
就像内敛函数可以解决被多个cpp引用一样,C++提出了内敛变量,有了inline static 我们可以解决这个问题:
使用了 inline修饰符之后,即使定义所在的头文件被多个 CPP 文件包含,也只会有一个全局对象,inline帮助我们在多个翻译单元中可以重复定义,帮助我们突破了ODR规则。
class A { public : inline static int a = 5 ; };
C++17后constexpr static成员现在隐含inline:
class A { public : constexpr static int a = 5 ; };
注意事项: inline帮助我们在多个翻译单元中可以重复定义,帮助我们突破了ODR规则,但也会带来UB:
但是请注意它带来的一些UB行为,常常多个inline不同的初始值会导致UB
inline int i = 2 ;inline int i = 5 ;int main () { std ::cout <<i<<std ::endl ; }
强制拷贝省略Mandatory Copy Elision C++17引入了一个新的规则:当以值传递或返回一个临时对象的时候必须省略对该临时对象的拷贝。
在之前拷贝省略一直是一种优化手段,常常与之相关的技术叫做RVO/NRVO等,C++17中开始强制省略临时变量拷贝,也就是即使禁用拷贝构造都可以,这在之前是不可以的。
int main () { foo(MyClass{}); MyClass x = bar(); foo(bar()); }
即使myclass的拷贝构造标为delete依然可以编译,这在C++17前是无法做到的。
有关Lambda constexpr lambda 就像函数一样,constexpr在C++17也对lambda进行了支持:
constexpr auto foo = [](int a, int b) { return a + b; };static_assert (6 == foo(2 , 3 ), "compile-time judge" );
向lambda传递this的拷贝 我们往往捕获this都是值捕获或是引用捕获,不过捕获的都是this指针,如果想获得this的拷贝:
在C++14可以使用广义lambda:
auto l1 = [thisCopy=*this ] { std ::cout << thisCopy.name << '\n' ; };
C++17后可以这样,更简单了:
auto l1 = [*this ] { std ::cout << name << '\n' ; };
std::as_const()常量引用捕获 auto printColl = [&coll = std ::as_const(coll)]
constexpr if 编译期if分支
template <typename T>std ::string asString (T x) { if constexpr (std ::is_same_v<T, std ::string >) { return x; } else if constexpr (std ::is_arithmetic_v<T>) { return std ::to_string(x); } else { return std ::string (x); } }
规定了求值顺序(带来的异常安全性) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> #include <unordered_map> #include <cstdio> int a () { return std ::puts ("a" ); }int b () { return std ::puts ("b" ); }int c () { return std ::puts ("c" ); }void z (int , int , int ) {}int main () { std ::unordered_map <int , int > m_map; m_map[0 ] = m_map.size (); std ::cout << "m_map[0]:" << m_map[0 ] << std ::endl ; z(a(), b(), c()); return a() + b() + c(); }
为了解决类似问题,C++17优化了求值顺序:
后缀表达式从左到右求值,包括函数调用 和成员选择表达式 ;
赋值表达式从右向左求值,包括复合赋值;
从左到右计算移位操作符的操作数。
String作为模板参数 template <const char * str>class Message { ... };extern const char hello[] = "Hello World!" ; const char hello11[] = "Hello World!" ; void foo () { Message<hello> msg; Message<hello11> msg11; static const char hello17[] = "Hello World!" ; Message<hello17> msg17; }
auto作为模板参数占位符 从C++17开始,你可以使用auto来声明一个非类型模板参数。比如:
template <auto N> class S { ... };
这允许我们针对不同类型都可以实例化非类型模板参数N:
然而,对于那些规则不允许的类型作为模板类型,这个特性仍然是没用的,即不会实例化成功:
折叠表达式 在C++17前,我们想要对C++11的参数包展开,可以进行递归展开,逗号表达式等方法,C++17后折叠表达式是一个很好的选择,可以对参数包进行操作。
递归展开参数包 在C++17前,我们想要对C++11的参数包展开,就需要递归展开:
void print () { std ::cout << std ::endl ; }template <typename T, typename ... Ts>void print (T arg, Ts... args) { std ::cout << arg << ", " ; print (args...); }int main () { print ("hello" , "string" , 1 , 2 , 3 ); return 0 ; }
即需要电一一个递归打印,和一个递归出口print。
用 逗号表达式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 template <typename T>void print_item (T arg) { std ::cout << arg << ", " ; }template <typename ... T>void expand (T... args) { int arr[] = { (print_item(args), 0 )... }; std ::initializer_list <int >{ (print_item(args), 0 )... }; std ::cout << std ::endl ; }int main () { expand("hello" , "string" , 1 , 2 , 3 ); return 0 ; }
回顾一下逗号表达式a = (b = c, d)
,表达式的执行顺序是,先进行b = c
的赋值,接着括号中的逗号表达式返回d
,因此最终表达式a
等于d
。
所以上面的例子中,int arr[] = { (print_item(args), 0)... };
语句,其实核心部分是这一句(print_item(args), 0)
,先指定print_item()
函数,然后逗号表达式返回0
;然后利用初始化列表(initialize list)来初始化一个初值都是0
、大小为sizeof...(args)
的数组。通过初始化列表,最终展开为
int arr[] = { (print_item(args0), 0 ), (print_item(args1), 0 ), (print_item(args2), 0 ), ... };
C++17后用fold expression 有四种语法
(pack op ...) (... op pack) (pack op ... op init) (init op ... op pack)
一元折叠
形如(E op ...)
的折叠表达式称为一元右折叠。
一元右折叠展开之后的含义是(E1 op (... op (En-1 op En)))
。
形如(... op E)
的折叠表达式称为一元左折叠。
一元左折叠展开之后的含义是(((E1 op E2) op ...) op En)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 template <typename ... T>auto sumR (T... args) { return (args + ...); }template <typename ... T>auto sumL (T... args) { return (... + args); }int main () { std ::cout << sumR(1 , 2 , 3 , 4 , 5 ) << std ::endl ; std ::cout << sumL(1 , 2 , 3 , 4 , 5 ) << std ::endl ; return 0 ; }
一元折叠
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 template <typename ... T>void printR (T... args) { ((std ::cout << args << ", " ), ...) << std ::endl ; }template <typename ... T>void printL (T... args) { (..., (std ::cout << args << ", " )) << std ::endl ; }int main () { printR("hello" , "zhxilin" , 1 , 2 , 3 ); printL("hello" , "zhxilin" , 1 , 2 , 3 ); return 0 ; }
在上面这个例子的折叠表达式中,op
是逗号,
,E
是输出语句std::cout << args << ", "
,所以其实是一个逗号表达式的折叠表达式。注意E
语句中<<
操作符的返回结果是std::ostream
,所以可以继续输出流操作。
二元折叠
二元折叠分为二元右折叠和二元左折叠。
假设表达式是E
(包含参数包),操作符是op
,初值是I
:
形如(E op ... op I)
的折叠表达式称为二元右折叠。
二元右折叠展开之后的含义是(E1 op (... op (En-1 op (En op I))))
。
形如(I op ... op E)
的折叠表达式称为二元左折叠。
二元左折叠展开之后的含义是((((I op E1) op E2) op ...) op En)
。
再看一下前面通过一元折叠表达式实现的累加函数,这种实现已经可以满足大多数情况了,但是有一种情况没法满足,就是函数传递了空参数。如果非要用一元折叠表达式实现,且支持传递空参数,那么需要对模板进行特化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 template <typename ... T>auto sumR (T... args) { return (args + ...); }auto sumR () { return 0 ; }template <typename ... T>auto sumL (T... args) { return (... + args); }auto sumL () { return 0 ; }int main () { std ::cout << sumR() << std ::endl ; std ::cout << sumL() << std ::endl ; return 0 ; }
虽然可行,但是还需要实现特化版本,还是比较麻烦的。更简单的方式是使用二元折叠表达式给定初值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 template <typename ... T>auto sumR (T... args) { return (args + ... + 0 ); }template <typename ... T>auto sumL (T... args) { return (0 + ... + args); }int main () { std ::cout << sumR() << std ::endl ; std ::cout << sumL() << std ::endl ; std ::cout << sumR(1 , 2 , 3 , 4 , 5 ) << std ::endl ; std ::cout << sumL(1 , 2 , 3 , 4 , 5 ) << std ::endl ; return 0 ; }
如此一来,只需要定义一个模板函数即可,不需要定义特化版本,就可以实现传空参数的累加求和了。
std::string_view C++17中我们可以使用std::string_view来获取一个字符串的视图,字符串视图并不真正的创建或者拷贝字符串,而只是拥有一个字符串的查看功能。std::string_view比std::string的性能要高很多,因为每个std::string都独自拥有一份字符串的拷贝,而std::string_view只是记录了自己对应的字符串的指针和偏移位置 。当我们在只是查看字符串的函数中可以直接使用std::string_view来代替std::string。
比如,最常见的substr,string_view效率会高很多。
std::variant<> 可以视为拥有类型安全的union。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <variant> #include <iostream> int main () { std ::variant<int , std ::string > var{"hi" }; std ::cout << var.index() << '\n' ; var = 42 ; std ::cout << var.index() << '\n' ; ... try { int i = std ::get <0 >(var); std ::string s = std ::get <std ::string >(var); ... } catch (const std ::bad_variant_access& e) { std ::cerr << "EXCEPTION: " << e.what() << '\n' ; ... } }
std::any
any_cast来转换类型
使用type()成员函数要注意是否开启了rtti
std ::any a; std ::any b = 4.3 ; a = 42 ; b = std ::string {"hi" }; if (a.type() == typeid (std ::string )) { std ::string s = std ::any_cast<std ::string >(a); useString(s); }else if (a.type() == typeid (int )) { useInt(std ::any_cast<int >(a)); }
std::optional<> std::optional<>
塑造了一个可容纳类型的、可空的对象。
比如:middle name 可以为空的一个场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Name { private : std ::string first; std ::optional<std ::string > middle; std ::string last; public : Name (std ::string f, std ::optional<std ::string > m, std ::string l) : first{std ::move (f)}, middle{std ::move (m)}, last{std ::move (l)} { } friend std ::ostream& operator << (std ::ostream& strm, const Name& n) { strm << n.first << ' ' ; if (n.middle) { strm << *n.middle << ' ' ; } return strm << n.last; } };int main () { Name n{"Jim" , std ::nullopt, "Knopf" }; std ::cout << n << '\n' ; Name m{"Donald" , "Ervin" , "Knuth" }; std ::cout << m << '\n' ; }