学习C++17的新特性
1.构造函数模板推导 
在之前,我们如果想用stl容器,都需要用<> 来手动指定参数类型。但在C++17中,我们不需要这么做了。
1 2 3 4 5 6 7 8 9 int  main ()     std::vector v1 = {1 ,2 ,3 ,4 };     std::pair p1  = {1 ,2.4234 };     cout << typeid (v1).name () << endl;     cout << typeid (p1).name () << endl;     return  0 ; } 
使用C++11编译,这个代码会报错。报错的意思是让我们指定参数的模板类型。
比如 std::pair p1  = {1,2.4234}; 在C++11中应该写成 std::pair<int,double> p1  = {1,2.4234};
1 2 3 4 5 6 7 8 test.cpp:16:10: error: use of class template 'std::pair' requires template arguments     std::pair p1  = {1,2.4234};          ^ /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_pair.h:211:12: note: template is declared here     struct pair            ^ 3 errors generated. make: *** [makefile:3: test] Error 1 
在C++17中,这样的写法就是可以被通过的了,也能正常推断出参数的类型,分别是一个int的vector,和一个int+double的pair;
1 2 3 4 5 $ make clang++ test.cpp -o test -std=c++17 $ ./test St6vectorIiSaIiEE St4pairIidE 
2.结构化绑定 
我们可以用 auto[变量1,变量2]的方式来接受一个tuple或者pair的返回值,将其绑定到两个不同的变量上。
tuple是C++11新增的一个数据结构,它和pair的用法类似,不同的是元组支持无数个参数。而pair仅支持两个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 std::tuple<int , double > func_tuple ()      return  std::tuple <int ,double >(1 , 2.2 ); } std::pair<int , double > func_pair ()      return  {1 ,2 }; } int  main ()     auto  [i, d] = func_tuple ();      cout << typeid (i).name () << endl;     cout << typeid (d).name () << endl;     cout << endl;     auto  [x,y] = func_pair ();     cout << typeid (x).name () << endl;     cout << typeid (y).name () << endl;     return  0 ; } 
使用C++11来编译,编译器会报错,但编译依旧能成功。这是因为我们的编译器是支持C++17的,但又被指定了-std=c++11,所以给用户报了个警告,但没有报错(因为这个语法在C++17里面是正确的)
1 2 3 4 5 6 7 8 clang++ test.cpp -o test -std=c++11 test.cpp:34:10: warning: decomposition declarations are a C++17 extension [-Wc++17-extensions]     auto [i, d] = func_tuple();           ^~~~~~ test.cpp:40:10: warning: decomposition declarations are a C++17 extension [-Wc++17-extensions]     auto [x,y] = func_pair();          ^~~~~ 2 warnings generated. 
运行输出结果如下
注意:结构化绑定不能应用于constexpr!
结构化绑定不止可以绑定pair和tuple,还可以绑定数组和结构体等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct  Point {     int  x;     int  y; }; Point func ()      return  {1 , 2 }; } int  main ()     int  array[3 ] = {1 , 2 , 3 };     auto  [a, b, c] = array;     cout << a << " "  << b << " "  << c << endl;          const  auto  [x, y] = func ();     return  0 ; } 
成功编译并输出结果
1 2 3 4 5 $ make clang++ test.cpp -o test -std=c++17 $ ./test 1 2 3 1 2 
自定义类型也能实现结构化绑定,这里从网上扒了一个代码下来,就不自己做测试了
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 29 30 31 32 33 34 class  Entry  {public :    void  Init ()           name_ = "name" ;         age_ = 10 ;     }     std::string GetName ()  const   { return  name_; }     int  GetAge ()  const  return  age_; } private :    std::string name_;     int  age_; }; template  <size_t  I>auto  get (const  Entry& e)      if  constexpr  (I == 0 )  return  e.GetName ()      else  if  constexpr  (I == 1 ) return  e.GetAge (); } namespace  std {    template <> struct  tuple_size <Entry> : integral_constant<size_t , 2 > {};     template <> struct  tuple_element <0 , Entry> { using  type = std::string; };     template <> struct  tuple_element <1 , Entry> { using  type = int ; }; } int  main ()      Entry e;     e.Init ();     auto  [name, age] = e;     cout << name << " "  << age << endl;      return  0 ; } 
3.if语句新增初始条件 
在之前我们都是用 if(判断条件) 来使用if语句的,C++17中给if新增了一个类似for循环中第一个参数的相同参数
比如
1 2 3 if (int  i=20 ;i<39 ){    cout <<"i<39!" <<endl; } 
运行效果如下
4.内联变量 
在之前我们想初始化一个类中的static变量,需要在类中定义,类外初始化。但如果是const的static变量,就能直接在类中通过缺省值的方式来初始化。
1 2 3 4 5 6 7 struct  A  {    static  int  value;       static  const  int  value_c=10 ;   }; int  A::value = 10 ;
在C++17中内联变量引入后,我们就可以直接实现在头文件中初始化static非const变量,或者直接用缺省值来初始化了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct  A {     static  int  value;     static  const  int  value_c = 10 ;      }; inline  int  A::value = 10 ;struct  B {     inline  static  int  value = 10 ;     inline  static  const  int  value_c = 10 ; }; 
相比于原本static变量初始化需要放到另外一个cpp源文件中,这种直接在头文件里面声明+初始化的方式能更好的确定变量的初始值。
5.折叠表达式 
C++17引入了折叠表达式使可变参数模板编程更方便:
1 2 3 4 5 6 7 8 template  <typename  ... Ts>auto  sum (Ts ... ts)      return  (ts + ...); } int  a {sum (1 , 2 , 3 , 4 , 5 )}; std::string a{"hello " }; std::string b{"world" }; cout << sum (a, b) << endl;  
实话说,可变模板参数这部分就没有弄明白过,实际上也没有用过,直接跳过!
6.constexpr+lambda表达式 
C++17前lambda表达式只能在运行时使用,C++17引入了constexpr lambda表达式,可以用于在编译期进行计算。
1 2 3 4 int  main ()      constexpr  auto  lamb = [] (int  n) { return  n * n; };     static_assert (lamb (3 ) == 9 , "a" ); } 
规则和普通的constexpr函数相同,参考我的C++11和14的文章。这里做简单说明:
constexpr修饰的函数体不能包含汇编语句、goto语句、label、try块、静态变量、线程局部存储、没有初始化的普通变量,不能动态分配内存,不能有new delete等,不能虚函数。
7.嵌套命名空间 
在之前如果需要嵌套命名空间,需要这样写
1 2 3 4 5 6 7 namespace  A {    namespace  B {         namespace  C {             void  func ()          }     } } 
C++17中可以直接用类似访问限定符的方式,前面加一个namespace来标明嵌套的命名空间。
1 2 3 4 namespace  A::B::C {    void  func ()  } 
8.__has_include预处理表达式 
1 2 3 4 5 6 #if  defined __has_include  #if  __has_include(<charconv> )  #define  has_charconv 1  #include  <charconv>   #endif  #endif  
如果一个代码会在多个不同的平台下跑,这个功能就很重要。比如我之前写项目的时候需要使用到jsoncpp,在centos和deepin下,安装jsoncpp的include路径是不同的
1 2 3 4 #include  <json/json.h>  #include  <jsoncpp/json/json.h>  
这种场景下就可以使用上面提到的这个预处理表达式进行判断,来确认你的jsoncpp路径到底在哪里。注意,这只能解决从yum和apt安装的jsoncpp,如果是自己手动安装的,那鬼知道你安装到哪里去了?🤣
所以很多大型项目如果需要使用jsoncpp这种第三方依赖项目,一般都会采用git submodule的方式,直接将第三方库下载到当前项目路径下,以避免不同平台的依赖项include路径不对而导致无法编译程序的问题。
9.this指针捕获(lambda) 
在lambda表达式中,采用[this]方式捕获的this指针是值传递 捕获的,但在一些情况下,会出现访问已经被释放了的空间的行为;比如如下代码
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 29 30 31 32 33 34 35 36 37 38 #include  <functional>  #include  <iostream>  #include  <memory>  using  namespace  std;struct  Foo {     std::unique_ptr<int > p;     std::function<void () > f ()        {        p.reset (new  int (10 ));         return  [&]         {             cout << 5  << endl;             cout << *p << endl;                                                     cout << 6  << endl;         };     } }; int  main ()     auto  foo = new  Foo ();     cout << 1  << endl;     auto  f = foo->f ();       cout << 2  << endl;     delete  foo;       cout << 3  << endl;          f ();       cout << 4  << endl;     return  0 ; } 
运行这个程序,可以看到是在*p的位置报错退出的;具体的原因参考代码中的注释。
1 2 3 4 5 6 $ ./test 1 2 3 5 Segmentation fault (core dumped) 
需要注意,lambda表达式中,使用=和&都会默认采用传值捕获this指针 ,因为this指针是存在于函数作用域中的一个隐藏参数,并不是独立在成员函数外的变量,所以是可以被捕捉到的;另外,this指针是不能被传引用捕获的,[&this] 的写法是不允许的;
1 2 3 4 5 clang++ test.cpp -o test -std=c++17 test.cpp:84:18: error: 'this' cannot be captured by reference         return [&this]                  ^ 1 error generated. 
C++17中提供了一个特殊的写法 [*this] 通过传值的方式捕获了当前对象本身,此时lambda表达式中存在的就是一个对象的拷贝,即便当前对象被销毁了,我们依旧可以通过这个拷贝访问到目标;
代码修改如下:
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 29 30 31 32 33 34 35 #include  <functional>  #include  <iostream>  #include  <memory>  using  namespace  std;struct  Foo {     std::shared_ptr<int > p;      std::function<void () > f ()        {        p.reset (new  int (10 ));         return  [*this ]         {             cout << 5  << endl;             cout << *p << endl;             cout << 6  << endl;         };     } }; int  main ()     auto  foo = new  Foo ();     cout << 1  << endl;     auto  f = foo->f ();       cout << 2  << endl;     delete  foo;       cout << 3  << endl;          f ();       cout << 4  << endl;     return  0 ; } 
此时重新编译,就能成功访问到指针p指向的对象了,并不受foo对象已经被delete的影响;
10.字符串转换 
没看懂这两个函数是干嘛的,找到的代码连编译都过不去,跳过吧
 
新增from_chars函数和to_chars函数
1 2 https://zh.cppreference.com/w/cpp/utility/from_chars https://blog.csdn.net/defaultbyzt/article/details/120151801 
11.std::variant 
C++17增加std::variant实现类似union的功能,但却比union更高级,举个例子union里面不能有string这种类型,但std::variant却可以,还可以支持更多复杂类型,如map等,看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int  main ()      std::variant<int , std::string> var ("hello" )  ;     cout << var.index () << endl;     var = 123 ;     cout << var.index () << endl;     try  {         var = "world" ;         std::string str = std::get <std::string>(var);          var = 3 ;         int  i = std::get <0 >(var);          cout << str << endl;         cout << i << endl;     } catch (...) {              }     return  0 ; } 
注意:一般情况下variant的第一个类型一般要有对应的构造函数,否则编译失败:
1 2 3 4 5 6 struct  A  {    A (int  i){}   }; int  main ()      std::variant<A, int > var;  } 
如何避免这种情况呢,可以使用std::monostate来打个桩,模拟一个空状态。
1 std::variant<std::monostate, A> var;  
12.std::optional 
https://en.cppreference.com/w/cpp/utility/optional 
有的时候,我们想在异常的时候抛出一个异常的对象,亦或者是在出现一些不可预期的错误的时候,返回一个空值。要怎么区分空值和异常的对象呢?
在python中,我们有一个专门的None对象可以来处理这件事。在MySQL中,我们也有NULL来标识空;但在CPP中,我们只剩下一个nullptr,其本质是个指针 ,与Py中的None和MySQL中的NULL完全不同!如果想用指针来区分空和异常对象,那就需要用到动态内存管理,亦或者是用智能指针来避免内存泄漏。
说人话就是,在CPP中没有一个类似None的含义为空的对象,来告诉调用这个程序的人,到底是发生了错误,生成了一个错误的对象,还是说压根什么都没有弄出来。
于是std::optional就出现了,其可以包含一个类型,并有std::nullopt来专门标识“空”这个含义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include  <optional>  std::optional<int > StoI (const  std::string &s)   {    try  {         return  std::stoi (s);     } catch (...) {         return  std::nullopt ;     } } void  func ()      std::string s{"123" };     std::optional<int > o = StoI (s);     if  (o) {         cout << *o << endl;     } else  {         cout << "error"  << endl;     } } 
这里我们进行了if的判断,首先判断变量o本身,为真代表的确返回了一个int值,为假代表返回的是nullopt;
随后再使用*o来访问到内部托管的成员。
需要注意这里是两层的逻辑关系,只有optional对象中成功托管了一个指定的参数类型,其本身才是真的。如果想访问它托管的对象,则需要用解引用。
比如这里,我们的o对象托管的是一个bool类型的假,但假并不代表空,o对象本身的判断是真,内部对*o的判断才是判断托管的bool值到底是真是假。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include  <optional>  int  main ()      std::optional<bool > o = false ;     cout << typeid (o).name () << endl;     if  (o)      {         if (*o){              cout << "true"  << endl;         }         else {             cout << "false"  << endl;         }     } else  {          cout << "error"  << endl;     }     return  0 ; } 
最终运行打印的结果是false;
13.std::any 
https://en.cppreference.com/w/cpp/utility/any 
这个类型可以托管任意类型的值,与之对应的还有一个std::any_cast来将其托管的值转成我们需要的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include  <any>  int  main ()      std::any a = 1 ;     cout << a.type ().name () << " "  << std::any_cast <int >(a) << endl;     a = 2.2f ;     cout << a.type ().name () << " "  << std::any_cast <float >(a) << endl;     if  (a.has_value ()) {         cout << a.type ().name ();     }     a.reset ();     if  (a.has_value ()) {         cout << a.type ().name ();     }     a = std::string ("a" );     cout << a.type ().name () << " "  << std::any_cast <std::string>(a) << endl;     return  0 ; } 
输出结果如下
1 2 3 i 1 f 2.2 fNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE a 
虽然any的出现让cpp也在一定程度上能实现“弱类型”变量,但在具体的开发中,明确变量的类型依旧比使用any好得多。特别是在变量的类型并不可以被直接转换的情况下。
14.std::apply 
使用std::apply可以将tuple/pair展开作为函数的参数传入,见代码:
1 2 3 4 5 6 7 8 9 10 11 #include  <tuple>  int  add (int  first, int  second)  return  first + second; }auto  add_lambda = [](auto  first, auto  second) { return  first + second; };int  main ()      std::cout << add (std::pair (1 , 2 )) << "\n" ;           std::cout << std::apply (add, std::pair (1 , 2 )) << '\n' ;     std::cout << std::apply (add_lambda, std::tuple (2.0f , 3.0f )) << '\n' ; } 
15.std::make_from_tuple 
使用make_from_tuple可以将tuple展开作为构造函数参数
1 2 3 4 5 6 7 8 9 struct  Foo  {    Foo (int  first, float  second, int  third) {         std::cout << first << ", "  << second << ", "  << third << "\n" ;     } }; int  main ()     auto  tuple = std::make_tuple (42 , 3.14f , 0 );    std::make_from_tuple <Foo>(std::move (tuple)); } 
16.std::string_view 
https://zhuanlan.zhihu.com/p/166359481 
https://en.cppreference.com/w/cpp/string/basic_string_view 
如果我们只需要一个string的只读类型的话,可以用string_view来托管。其内部只包含一个指向目标字符串的指针,以及字符串的长度。
string_view内部封装了string的所有只读接口,本来就是给你读的。
需要注意的是,因为内部只有一个指针,所以当string_view托管的string被销毁了,与之关联的所有string_view都会失效!同样是因为内部只有一个指针和字符串的长度两个变量,所以在传值拷贝的时候,string_view的效率会高很多。
这和const string& 类型的传值又有什么区别呢?传引用不是也没有拷贝消耗吗? 
 
这个问题很好,我不知道!百度也没有百度出来……
我能想到的就是用string_view作为参数的时候,如果入参是一个常量字符串,此时不需要构造string,而使用const string& 接受常量字符串的时候依旧需要构造一个string对象。这部分就会有一定的消耗。
17.as_const 
C++17使用as_const可以将左值转成const类型
1 2 std::string str = "str" ; const  std::string& constStr = std::as_const (str);
18.file_system 
C++17正式将file_system纳入标准中,提供了关于文件的大多数功能,基本上应有尽有,这里简单举几个例子:
1 2 3 4 5 namespace  fs = std::filesystem;fs::create_directory (dir_path);  fs::copy_file (src, dst, fs::copy_options::skip_existing);  fs::exists (filename);  fs::current_path (err_code);  
19.shared_mutex 
这玩意是个读写锁。简单介绍一下什么是读写锁:
读者可以有多个,写者只能有一个 
写锁是互斥的,如果A有锁,B想拿锁就得阻塞等待 
读锁是共享的,C有读锁,D也想读,两个人可以一起看 
读写锁是互斥的,有人写的时候不能读,有人读的时候不能写 
 
换到专业术语上,就是分为独占锁(写锁)和共享锁(读锁);
在C++14中其实已经有了一个shared_timed_mutex,C++17中这个锁的操作与其基本一致,只不过多了几个和时间相关的接口
1 2 3 4 try_lock_for (...);try_lock_shared_for (...);try_lock_shared_until (...);try_lock_until (...);
具体使用可以参考
1 2 https://zh.cppreference.com/w/cpp/thread/shared_mutex https://zhuanlan.zhihu.com/p/610781321 
The end 
关于C++17常用的基本就是这些了,后续遇到新的再更新本文。