学习C++14的那些新特性
为了方便指定使用C++14来编译代码,本文的测试都是在linux下进行的,g++版本如下
1 2 $ g++ --version g++ (GCC) 8.5.0 20210514 (Red Hat 8.5.0-4)
如果你和我一样,也是使用VSC来链接linux进行代码编写,那一定要记得修改C++插件里面的CPP版本,否则默认以C++11来进行语法高亮的话,会把C++11不支持的语法标红,影响我们学习
本文参考 https://zhuanlan.zhihu.com/p/588826142 进行学习;
官方文档 https://zh.cppreference.com/w/cpp/14
1.lambda新特性 C++14给lambda表达式添加了两个新功能
参数推断(auto) 参数初始化后捕获(可以在[]
对某个新参数进行赋值) 先来复习一下C++11中学习的lambda捕获的基本方式
1 2 3 4 5 [val]:表示值传递方式捕捉变量val [=]:表示值传递方式捕获所有父作用域中的变量(包括this) [&val]:表示引用传递捕捉变量val [&]:表示引用传递捕捉所有父作用域中的变量(包括this) [this]:表示值传递方式捕捉当前的this指针
在C++14中,新增的是下面的这种情况
1 2 3 4 5 int a = 30 ;auto func = [x = 3 ](auto y) {return x + y; };cout << func (a) << endl;
运行测试,可以看到成功输出了结果
1 2 3 4 $ make g++ test.cpp -o test -std=c++14 $ ./test 33
修改一下类型,也能正常调用
1 2 3 4 double a = 30.2 ;auto func = [x = 3 ](auto y){ return x + y; }; cout << func (a) << endl;
如果想将赋值参数和原本的捕获方式一起使用,则需要将赋值参数 放在[]
的最后面
1 2 3 4 5 6 7 8 9 10 11 void test_lambda2 () { int a = 10 , b = 20 ; int c = 1 , d = 3 , e = 5 ; auto func6 = [=, f = 30 , g = 40 ] { return (a + b + c + d + e + f + g); }; cout << func6 () << endl; }
初始化捕获的好处是可以支持移动捕获 了;不然在C++11中,lambda就只能使用赋值捕获和引用捕获
1 2 3 std::unique_ptr<Item> item (new Item()) ;auto func = [m = std::move (item)] { };
这个新特性的提出,也让lambda成功有了和bind比拼的能力。在C++11中,bind的优势就是在于移动捕获 的支持;如今lambda也有了这份能力了,我们可以更灵活地根据场景选用lambda或者bind,而不是只能使用bind了。
2.变量模板 2.1 示例 看清楚这个名字啊!是变量模板 ,可不是什么函数模板哈!
1 2 3 4 5 6 7 8 9 template <class T >T pi = T (3.1415926535897932385L ); void test_value_template () { cout << pi<double > << endl; cout << pi<float > << endl; cout << pi<int > << endl; }
如上就是一个最最最简单的变量模板,我们在传入对应的类型后,他就会转成我们需要的类型
1 2 3 4 5 6 $ make g++ test.cpp -o test -std=c++14 $ ./test 3.14159 3.14159 3
2.2 类中使用 当你需要在类中使用模板变量的时候,这个变量必须定义为static
;
因为它是模板,我们还可以接用模板本身就有的特性,将这个模板针对某一个类型进行特化
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 struct Limits { template <typename T> static const T min; }; template <typename T>const T Limits::min = { }; template <>const float Limits::min<float > = 4.5 ;template <>const double Limits::min<double > = 5.5 ;template <>const std::string Limits::min<std::string> = "hello" ;int main () { std::cout << Limits::min<int > << std::endl; std::cout << Limits::min<float > << std::endl; std::cout << Limits::min<double > << std::endl; std::cout << Limits::min<std::string> << std::endl; return 0 ; }
1 2 3 4 5 $ ./test 0 4.5 5.5 hello
2.3 和类型转换的区别 这里我又直接定义了一个变量,使用static_cast
直接转换变量,看看结果会不会有什么区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 template <class T >T pi = T (3.1415926535897932385L ); long double lpi = 3.1415926535897932385L ; void test_value_template () { cout << pi<double > << endl; cout << pi<float > << endl; cout << pi<int > << endl; cout << " ----- \n" ; cout << static_cast <double >(lpi) << endl; cout << static_cast <float >(lpi) << endl; cout << static_cast <int >(lpi) << endl; }
看上去二者的结果完全相同,那么既然可以直接使用变量类型转换,为什么还要新增一个模板变量呢?
1 2 3 4 5 6 7 8 $ ./test 3.14159 3.14159 3 ----- 3.14159 3.14159 3
以下内容来自GPT,我觉得它说的很对
定义一个变量并使用数据转换(类型转换)是一种常见的编程方式,但与变量模板有一些区别:
通用性: 变量模板允许你通过模板参数来生成多个不同类型的变量,从而在不同的上下文中使用。这使得代码更具通用性和可扩展性,因为你可以为多个类型生成相应的变量。相比之下,直接定义变量并使用数据转换通常只适用于特定的一种数据类型。模板化: 变量模板是一种模板化的方式来生成变量,它遵循 C++ 的模板机制,这意味着你可以使用模板特化 、部分特化等技术来定制化生成的变量,以满足不同的需求。而使用数据转换时,你必须显式地执行类型转换,这可能会在代码中引入不必要的重复。编译时计算: 变量模板通常用于在编译时生成值,因此可以在编译阶段进行类型检查和计算。这有助于提高代码的性能和安全性。而数据转换可能在运行时进行,可能会引入一些运行时开销和类型错误的风险。抽象性: 变量模板可以在更高的抽象层次上操作数据,使代码更具表达力和可读性。它允许你以更自然的方式描述某个值与特定类型之间的关系,而不必显式进行类型转换。总之,变量模板提供了一种更灵活、通用和模板化的方式来生成变量,适用于需要在不同类型上工作的情况。当你需要为多个类型生成特定的变量或值时,变量模板是一种更优雅和强大的选择。
3.constexpr限制放宽 在C++11中被引入的constexpr,可以让编译器在编译程序的期间,就将一部分工作完成,不必等到运行期间再做;在C++11中,constexpr的限制很严格,这导致它并不好用:
constexpr修饰变量,要求变量必须可以在编译器推导出来 constexpr修饰函数(返回值),函数内除了可以包含using和typedef指令以及static_asssert
断言外,只能包含一条return
语句 constexpr同时可以修饰构造函数,但也会要求使用这个构造函数的时候,可以在编译器就把相关的内容全推导出来 以下是一个比较基础的C++11中的用例,给该函数设置了constexpr
关键字后,该函数就可以在编译期间被计算出结果,再用static_assert
在编译期间断言结果是否正确;
1 2 3 4 5 6 7 8 9 10 constexpr int factorial (int n) { return (n <= 1 ) ? 1 : n * factorial (n - 1 ); } int test_constexpr1 () { constexpr int result = factorial (5 ); static_assert (result == 120 , "Factorial of 5 should be 120" ); cout << result << endl; return 0 ; }
如果在C++11中的constexpr函数内包含其他语句,编译的时候会报错,翻译过来是该函数内部不是一个return返回语句
1 2 3 4 5 $ g++ test.cpp -o test -std=c++11 test.cpp: In function ‘constexpr int FuncNew(int)’: test.cpp:96:1: error: body of ‘constexpr’ function ‘constexpr int FuncNew(int)’ not a return-statement } ^
c++14中,对constexpr的限制放宽了,允许使用循环、if、switch 等等语句,但是主旨还是一样的,需要在编译期间就可以计算出全部内容;限制放宽之后,这个关键字便可以更灵活的使用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 constexpr int Func (int n) { return n > 0 ? Func (n - 1 ) + n : 0 ; } constexpr int FuncNew (int n) { if (n <= 0 ) { return 0 ; } int sum = 0 ; for (int i = 0 ; i < n; ++i) { sum += i; } return sum; }
4.二进制变量 可以使用0b
或者0B
开头直接定义二进制变量。
1 2 3 4 5 6 int main () { int bit1 = 0b1001 ; int bit2 = 0B 1011; std::cout << bit1 << " " << bit2 << std::endl; }
运行结果如下
1 2 3 $ g++ test.cpp -o test -std=c++14 $ ./test 9 11
我在测试中发现,当我用C++11编译此代码的时候,似乎也没有引发编译错误,难道说0b是在C++11里面就支持了吗?
1 2 3 $ g++ test.cpp -o test -std=c++11 $ ./test 9 11
GPT给出了0B这种二进制变量是在C++14中引入的确认,并提到了为什么会出现上述情况;虽然C++11看上去编译和运行都没有问题,但我们还是得遵循版本,选用正确的版本进行编译,才能根本上避免错误
C++标准通常是向后兼容的,这意味着较新版本的编译器通常会继续支持较旧版本的标准。例如,如果你在使用支持C++11标准的编译器(如g++)时,使用了C++14或更高版本的特性,通常不会引发编译错误,因为这些编译器会尽量向后兼容,以保持现有代码的可编译性。
在你提到的情况下,即使你使用g++编译器以C++11标准编译,它仍然可以理解和接受C++14引入的二进制字面量特性。这是编译器开发者的一种设计选择,以便使代码的迁移更加平滑。但是,为了遵循最佳实践和保持代码的可读性,当你在使用特定C++标准的功能时,最好将编译器选项设置为该标准的版本,以确保代码的可移植性。
5.数字分隔符 在日常生活中使用数字的时候,为了更好的可读性,我们会以3个数组或者4个数组为分割,打一个点
1 2 1,0000,0000 一亿 100,000,000
C++14中,也支持了这样的打点,以方便我们更好的看出大数字的位数
1 2 3 4 5 6 7 8 9 void test_num_div () { long long big_num1 = 100000000 ; long long big_num2 = 100'000'000 ; long long big_num3 = 1'0000'0000 ; cout << big_num1 << endl; cout << big_num2 << endl; cout << big_num3 << endl; }
需要注意,这样的操作不会对数字本身有任何影响
1 2 3 4 $ ./test 100000000 100000000 100000000
在C++11中这种语法是不支持的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ g++ test.cpp -o test -std=c++11 test.cpp:116:29: warning: multi-character character constant [-Wmultichar] long long big_num2 = 100'000'000; ^~~~~ test.cpp:117:27: warning: multi-character character constant [-Wmultichar] long long big_num3 = 1'0000'0000; ^~~~~~ test.cpp: In function ‘void test_num_div()’: test.cpp:116:29: error: expected ‘,’ or ‘;’ before '\x303030' long long big_num2 = 100'000'000; ^~~~~ test.cpp:117:27: error: expected ‘,’ or ‘;’ before '\x30303030' long long big_num3 = 1'0000'0000; ^~~~~~
6.返回值auto推导 c++14新增了函数返回值的推导,当返回值声明为auto时,编译器会根据你的return语句推导出你的返回值类型。
1 2 3 4 5 6 7 8 9 10 11 12 template <typename T>auto Func (T x, T y) { return x + y; } int main () { std::cout << Func (3 , 4 ) << std::endl; std::cout << Func (3.1 , 4.2 ) << std::endl; return 0 ; }
1 2 3 4 5 $ make g++ test.cpp -o test -std=c++14 $ ./test 7 7.3
这个推导是有限制条件的
1、如果有多个推导语句,那么多个推导的结果必须一致
1 2 3 4 5 6 7 8 9 10 11 12 auto Func (int flag) { if (flag < 0 ) { return 1 ; } else { return 3.14 ; } }
2、如果没有return或者return为void类型,那么auto会被推导为void。
1 2 3 auto f () {} auto g () { return f (); } auto * x () {}
3、一旦在函数中看到return语句,从该语句推导出的返回类型就可以在函数的其余部分中使用,包括在其他return语句中。
1 2 3 4 5 6 7 8 9 10 11 auto Sum (int i) { if (i <= 1 ) { return i; } else { return Sum (i - 1 ) + i; } }
但是如果还没被推导出来,那就不能使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 auto Sum (int i) { if (i > 1 ) { return Sum (i - 1 ) + i; } else { return i; } } error: use of ‘auto Sum (int ) ’ before deduction of ‘auto ’
4、不能推导初始化列表。
1 2 3 4 auto func () { return {1 , 2 , 3 }; }error: returning initializer list
5、虚函数不能使用返回值推导
1 2 3 4 5 6 7 struct Item { virtual auto Func () ; }; error: virtual function cannot have deduced return type
7.[[deprecated]]
标记 这个标记的作用是告知其他人,某个函数被弃用了,不允许继续调用该函数;该字段的好处在于,如果一个方法已经在后续不需要使用了,你可以先给他加上这个关键字,然后再进行其他的代码检查,确认无误后,再将这个函数整体清除;
别人也不需要去检查函数的实现,因为在编译过程中编译器就会告诉你这个函数被弃用;但是编译依旧是成功的 !
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [[deprecated]] int test_return_auto () { std::cout << Func (3 , 4 ) << std::endl; std::cout << Func (3.1 , 4.2 ) << std::endl; return 0 ; } int main () { test_return_auto (); return 0 ; }
在编译的时候,编译器会警告你,这个函数已经被弃用了;但这里只是警告,编译依旧成功了,所以最终还是需要程序猿去瞅一眼各个警告到底是什么意思。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ make g++ test.cpp -o test -std=c++14 test.cpp: In function ‘int main()’: test.cpp:145:22: warning: ‘int test_return_auto()’ is deprecated [-Wdeprecated-declarations] test_return_auto(); ^ test.cpp:132:5: note: declared here int test_return_auto() ^~~~~~~~~~~~~~~~ test.cpp:145:22: warning: ‘int test_return_auto()’ is deprecated [-Wdeprecated-declarations] test_return_auto(); ^ test.cpp:132:5: note: declared here int test_return_auto() ^~~~~~~~~~~~~~~~
std库的新特性 以下是STD库的新增内容!
8.std::make_unique 这个东西在cplusplus网站上找不到释义,所以就去cpp的官网 上找了
https://zh.cppreference.com/w/cpp/memory/unique_ptr/make_unique
该函数定义在<memory>
头文件中
1 2 3 4 5 6 7 8 9 template < class T, class ... Args >unique_ptr<T> make_unique ( Args&&... args ) ;template < class T >unique_ptr<T> make_unique ( std::size_t size ) ;template < class T , class ... Args > make_unique ( Args&&... args ) = delete ;
作用是构造 T
类型对象并将其包装进 std::unique_ptr ;
参数 说明 args 将要构造的 T
实例所用的参数列表。 size 要构造的数组大小
构造非数组类型 T
对象。传递参数 args
给 T
的构造函数。此重载只有在 T
不是数组类型时才会参与重载决议。函数等价于: 1 unique_ptr <T>(new T (std::forward<Args>(args)...))
构造拥有动态大小的数组。值初始化 数组元素。此重载只有在 T
是未知边界数组时才会参与重载决议。函数等价于: 1 unique_ptr <T>(new std::remove_extent_t <T>[size]())
使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 class test_class {public : test_class (int a=-1 ):_a(a){} int _a; }; int main () { std::unique_ptr<test_class> pt = std::make_unique <test_class>(3 ); cout << pt->_a << endl; return 0 ; }
1 2 3 4 $ make g++ test.cpp -o test -std=c++14 $ ./test 3
9.std::shared_timed_mutex与std::shared_lock c++11引入了多线程线程的一些库,但是是没有读写锁的,因此在c++14引入了读写锁的相关实现(头文件shared_mutex),其实c++14读写锁也还不够完善,直到c++17读写锁这块才算是完备起来。
std::shared_timed_mutex
是带超时的读写锁对象,接口还算比较简洁易懂,和之前接触过的其他锁基本一致;内部成员中lock()
是写锁,lock_shared()
是读锁;
https://zh.cppreference.com/w/cpp/thread/shared_timed_mutex
std::shared_lock
是加锁的RAII实现,即构造时加锁,析构时解锁;我们使用shared_lock/unique_lock
来从shared_timed_mutex
中获取锁的时候,就会自动获取读锁和写锁;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 std::shared_timed_mutex mutex; void readOperation () { std::shared_lock<std::shared_timed_mutex> lock (mutex) ; std::cout << "Read operation: " << sharedResource << std::endl; } void writeOperation () { std::unique_lock<std::shared_timed_mutex> lock (mutex) ; sharedResource++; std::cout << "Write operation: " << sharedResource << std::endl; }
10.std::exchange c++14新增了一个接口std::exchange
(头文件utility),其实这个也并不算是新增的,因为这个接口其实在c++11的时候就有了,只不过在c++11中作为一个内部函数,不暴露给用户使用,在c++14中才把它暴露出来给用户使用。使用方法也很简单。
1 2 3 4 5 6 7 8 9 10 11 int main () { std::string s1 = "hello" ; std::string s2 = "world" ; std::exchange (s1, s2); std::cout << s1 << " " << s2 << std::endl; return 0 ; } world world
我们可以看到,exchange会把第二个值赋值给第一个值,但是不会改变第二个值。我们来看下它的实现吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 template <typename _Tp, typename _Up = _Tp> _GLIBCXX20_CONSTEXPR inline _Tp exchange (_Tp& __obj, _Up&& __new_val) noexcept (__and_<is_nothrow_move_constructible<_Tp>, is_nothrow_assignable<_Tp&, _Up>>::value) { return std::__exchange(__obj, std::forward<_Up>(__new_val)); } template <typename _Tp, typename _Up = _Tp> _GLIBCXX20_CONSTEXPR inline _Tp __exchange(_Tp& __obj, _Up&& __new_val) { _Tp __old_val = std::move (__obj); __obj = std::forward<_Up>(__new_val); return __old_val; }
通过注释我们可以明白含义,它的作用是把第二个值赋值给第一个值,同时返回第一个值的旧值。
除此之外,我们这里说明一个关键的点。exchange的第二个参数是万能引用,所以说他是既可以接收左值,也可以接收右值的,所以我们可以这样来使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int main () { std::string s1 = "hello" ; std::exchange (s1, "world" ); std::cout << s1 << std::endl; std::string s2 = "hello world" ; std::exchange (s1, std::move (s2)); std::cout << s1 << " | " << s2 << std::endl; return 0 ; } world hello world |
11.std::integer_sequence 类模板 std::integer_sequence
表示一个编译时的整数序列。在用作函数模板 的实参时,能推导参数包 Ints
并将它用于包展开。
https://zh.cppreference.com/w/cpp/utility/integer_sequence
这个实在是太难懂了,搞不明白是干嘛的,放弃了😥
12.std::quoted https://zh.cppreference.com/w/cpp/io/manip/quoted
该函数模板位于 <iomanip>
头文件中,用于在输入输出流中处理被引号包围的字符串。它通常用于处理 CSV(逗号分隔值)文件或其他格式,其中字段被引号括起来以处理包含特殊字符(如逗号、换行符等)的情况。
对于cout
而言,quoted会将字符串包围在双引号中输出
1 2 3 4 5 6 7 int test_quorted () { std::string data = "Hello, \"world\"\n" ; std::cout << std::quoted (data) << std::endl; return 0 ; }
1 2 3 $ ./test "Hello, \"world\" "
以下是官方给的一个示例
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 #include <iostream> #include <iomanip> #include <sstream> int main () { std::stringstream ss; std::string in = "String with spaces, and embedded \"quotes\" too" ; std::string out; auto show = [&](const auto & what) { &what == &in ? std::cout << "read in [" << in << "]\n" << "stored as [" << ss.str () << "]\n" : std::cout << "written out [" << out << "]\n\n" ; }; ss << std::quoted (in); show (in); ss >> std::quoted (out); show (out); ss.str ("" ); in = "String with spaces, and embedded $quotes$ too" ; const char delim {'$' }; const char escape {'%' }; ss << std::quoted (in, delim, escape); show (in); ss >> std::quoted (out, delim, escape); show (out); }
输出
1 2 3 4 5 6 7 read in [String with spaces, and embedded "quotes" too] stored as ["String with spaces, and embedded \"quotes\" too"] written out [String with spaces, and embedded "quotes" too] read in [String with spaces, and embedded $quotes$ too] stored as [$String with spaces, and embedded %$quotes%$ too$] written out [String with spaces, and embedded $quotes$ too]
在给定的代码中,delim
和 escape
是用于指定自定义的分隔符和转义字符的参数。这些参数是用于 std::quoted
函数的重载形式,允许你指定不同于默认引号的字符来包围字符串,并指定一个不同于默认转义字符的字符来转义引号字符。以下是关于这两个参数的详细解释:
delim
: 分隔符 在第一个用法中,std::quoted
函数使用了三个参数的重载形式:std::quoted(in, delim, escape)
。delim
参数用于指定包围字符串的分隔符。通常情况下,std::quoted
使用双引号作为默认分隔符,但在某些情况下,你可能想要使用其他字符来包围字符串,以避免与字符串本身的字符冲突。在你的代码示例中,分隔符 delim
被设置为 $
,这意味着字符串会被包围在 $
字符内。escape
: 转义字符 escape
参数允许你指定一个字符,用于转义分隔符字符本身。在默认情况下,std::quoted
使用双引号 "
作为转义字符,以确保在字符串中嵌入的引号不会被解释为结束引号。但如果你选择了自定义的分隔符,你可能还需要指定一个不同于默认转义字符的字符来进行转义。在你的代码示例中,转义字符 escape
被设置为 %
,这意味着在字符串中,如果你想要表示分隔符 $
本身,你需要使用 %$
。这部分也不是很容易搞明白它是干嘛的,如果面试官问道了就说我不会吧😭