学习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常用的基本就是这些了,后续遇到新的再更新本文。