Ubuntu 18.04 自带 gcc 7.3 支持 c++14
查看编译器支持:
- c++11 https://gcc.gnu.org/projects/cxx-status.html#cxx11
- c++14 https://gcc.gnu.org/projects/cxx-status.html#cxx14
- c++17 https://gcc.gnu.org/projects/cxx-status.html#cxx1z
- c++11 feature
- nullptr/constexpr
- enum class
- auto/decltype
- for iteration
- initialize_list
- lamda
- template
- rvalue/move
- nullptr
以前的编译器实现, 可能会把 NULL 定义为 0. 所以, 当你有两个同名函数 foo(int),foo(char*) 时, foo(NULL) 你的本意可能是调用后者, 但实际调用的是前者. nullptr 的引入就是为了解决这个问题.
- void foo(char *ch)
- {
- std::cout <<"call foo(char*)" << std::endl;
- }
- void foo(int i)
- {
- std::cout << "call foo(int)" << std::endl;
- }
- void test_nullptr()
- {
- if (NULL == (void *)0)
- std::cout << "NULL == 0" << std::endl;
- else
- std::cout << "NULL != 0" << std::endl;
- foo(0);
- //foo(NULL); // 编译无法通过
- foo(nullptr);
- }
- constexpr
常量表达式的引入是为了提高性能, 将运行期间的行为放到编译期间去完成.如下代码
- constexpr long int fib(int n)
- {
- return (n <= 1)? n : fib(n-1) + fib(n-2);
- }
- void test_constexpr()
- {
- auto start = std::chrono::system_clock::now();
- const long int res = fib(30);
- auto end = std::chrono::system_clock::now();
- std::chrono::duration<double> elapsed_seconds = end-start;
- cout <<"elapsed_seconds:"<<elapsed_seconds.count()<<endl;
- start = std::chrono::system_clock::now();
- long int res2 = fib(30);
- end = std::chrono::system_clock::now();
- elapsed_seconds = end-start;
- cout << "elapsed_seconds:"<<elapsed_seconds.count()<<endl;
由于传给 fib(int n) 的参数是 30, 是个固定的值.所以可以在编译期间就求出来. 当我们用 const long 和long 声明返回值时, 对前者会在编译期就做计算优化. 如下图, 可以看到二者运行时间有数量级上的差异.
enum class
c++11 中把枚举认为是一个类, 以前的标准中枚举值就是一个整数而已.看如下代码
- void test_enum()
- {
- enum color {black,white};
- //auto white = true; //redeclared
- enum class Color{r,g,b};
- auto r = 1;
- }
以前的标准中 enum color {black,white}; 相当于定义了两个 int 变量 black,white. 所以 //auto white = true; 编译期会报错.引入了 enum class 则不再有这个问题.
auto/decltype
这个是c++11 中非常重要的一点特性, 极大地简化了编码的复杂. 编译期自动去推导变量的类型.再也不需要我们操心了.
auto 做变量类型推导, decltype 做表达式类型推导.
- void test_auto()
- {
- std::vector<int> v;
- v.push_back(1);
- v.push_back(2);
- for (std::vector<int>::iterator it = v.begin(); it != v.end(); it++)
- {
- cout <<*it << endl;
- }
- for (auto it = v.begin(); it != v.end(); it++)
- {
- cout << *it << endl;
- }
- for (auto &i : v)
- {
- cout << i << endl;
- i = 100; // 修改掉 v 中的元素值
- }
- for (auto i : v)
- {
- cout << i << endl; // 输出 100
- i = 200; // 不会修改 v 中的元素值
- }
- for (auto i : v)
- {
- cout << i << endl; // 输出为 100
- }
- }
用法如上述代码所示. 比如遍历vector, 写法由 for (std::vector::iterator it = v.begin(); it != v.end(); it++) 简化到 for (auto it = v.begin(); it != v.end(); it++), 如果配合上 for 迭代, 则进一步简化到 for (auto &i : v).
注意c++11 中, auto 变量自动推导有2个例外
- //int add(auto x,auto y); //c++14 才支持函数参数为 auto
- //auto arr[10] = {
- 0
- }; // 编译错误 auto 不能用于数组类型的推导
decltype 做表达式类型推导. 假设我们要写一个加法的模板函数, 比如
- template<typename R, typename T, typename U>
- R add(T x, U y)
- {
- return x+y;
- }
对于返回值类型R的话, 我们必须在模板的参数列表中手动指明. 调用的时候形式则为 add<R,T,U>(x,y). 而很可能我们并不知道返回类型是什么, 比如我使用一个第三方库, 我只是想对x,y 做一个 add 操作, 后续我可能会从x,y 取一些数据做后续处理, 此时我并不关心 add 操作返回值类型是什么.
c++11 中用 decltype 自动推导表达式类型解决这个问题.
- template<typename T, typename U>
- auto add_cxx11(T x, U y) -> decltype(x+y)
- {
- return x+y;
- }
用 decltype(x+y) 声明返回值类型, 让编译器自动推导就好了.
在c++14 中, 有了更好的支持, 已经不再需要显示地声明返回值类型了.
- //c++14 支持
- /*
- template<typename T, typename U>
- auto add_cxx14(T x, U y)
- {
- return x+y;
- }
- */
for 迭代
基于范围的 for 迭代, 非常类似与 python 中的用法了.代码在前面 auto/decltype 一节已经展示. 需要注意的是 for (auto i : v) 拿出的 i 是副本, 不会修改 v 中的元素的值.for (auto &i : v) 拿到的是引用, 会修改掉 v 中的值.
初始化列表
c++11 之前对象的初始化并不具有统一的表达形式, 比如
- int a[3] = {4,5,6}
- /*
- 以前类的初始化只能通过拷贝构造函数或者 ()
- 比如 */
- class A
- {
- A(int x,int y,int z)
- }
- A b;
- A a(b);
- A a2(1,2,3)
c++11 提供了统一的语法来初始化任意对象.
比如
- class XX
- {
- public:
- XX(std::initializer_list<int> v):v_int(v)
- {
- }
- vector<int> v_int = {3,4,5};
- };
- XX xxxxxx = {6,7,8,9,10};
或者
- struct A
- {
- int a_;
- int b_;
- };
- A a {1,2};
此外, 初始化列表还可以作为函数的入参.
- void f_take_initialize(initializer_list<int> list)
- {
- int sum = 0;
- for (auto l : list)
- {
- sum += l;
- }
- cout <<sum << endl;
- }
- void test_initialize()
- {
- f_take_initialize({1, 2, 3});
- }
using 关键字
using 并不是c++11 才有的, 但是c++11 中提升了这个关键字的功能, 用于取代 typedef, 提供更加统一的表达形式.
- template <typename T, typename U>
- class SuckType;
- typedef SuckType<std::vector<int>, std::string> NewType;
- using NewType = SuckType<std::vector<int>, std::string>;
- typedef int(*mycallback)(void*);
- using mycallback = int(*)(void*);
lamda 表达式
即匿名函数, 这也是c++11 中一个相当重要的特性.有的时候, 我们可能需要使用某个功能, 但这个功能可能只在某一个函数内部有用, 那么我们则没有必要去写一个全局函数或者类的成员函数去抽象这个功能.这时候就可以实现一个匿名函数.
[捕获列表](参数列表) -> 返回类型
- {
- // 函数体
- }
匿名函数的形式如上所示.参数列表, 返回类型都很好理解.默认情况下, 匿名函数是不可以使用匿名函数外部的变量的, 捕获列表就起到一个传递外部参数的作用
捕获列表有下述几种情况
值捕获
引用捕获
隐式捕获
表达式捕获 //c++14 支持
值得注意的是, 值捕获的话, 值是在匿名函数定义好的时候就做传递, 而不是调用的时候做传递.
如下代码
- void test_lamda()
- {
- auto add_func = [](int x,int y) -> int{return x+y;};
- auto sum = add_func(3,4);
- cout<<"sum="<<sum<<endl;
- int a = 1;
- auto copy_a = [a]{return a;};
- a = 100;
- auto a_i_get = copy_a();
- printf("a_i_get=%d,a=%d\n",a_i_get,a);
auto copy_a_refrence = [&a]{a=50;return a;};
- auto a_refrence_i_get = copy_a_refrence();
- printf("a_i_get=%d,a=%d\n",a_refrence_i_get,a);
- int b = 1;
- auto f = [&](){return b;} ;
- }
注意, 第一个输出的 a_i_get=1 而不是 100. 尽管a是 100.这是因为在 copy_a 定义的时候, a 的值是 1. copy_a_refrence 的捕获列表是引用, 函数体内修改 a=50, 所以输出的是 50
当要捕获的变量非常多的时候, 一个个写是非常麻烦的, 所以可以直接在捕获列表里用 = 和 & 表示传值和传引用
- [](){
- } // 传空
- [=](){
- } // 传值
- [&](){
- } // 传引用
变长模板
template<typename... Ts> class Magic;
c++11 之前, 模板的参数是固定个数的.c++11 之后支持不定长参数的模板. 用... 表示不定长.
c++11 标准库新引入的数据结构 tuple 就是用了这个特性实现的.
move 语义和右值引用.
这也是 c++11 中引入的非常重要的一个特性. 主要作用在于性能的提升.
通俗地讲, 一个可以取地址的变量, 即为左值, 不可以取地址的即为右值.
以之前的 vector.push_back() 为例, 插入的是数据的一份拷贝. 当要插入的数据结构本身内存特别大的时候, 这种拷贝带来的性能消耗是非常大的.
move 的引入即用于解决此类问题, move() 将一个值转换为右值. 标准库的数据结构里实现了 void push_back( T&& value ); 的版本.
move() 名字叫 move, 但他本身并不做任何 move 的操作, move 更类似于浅拷贝, 即拷贝待拷贝内容的地址过去.但是浅拷贝并不会使得之前的对象失去所有权, 而 move 会, 所以 move 所做的事情就是资源的所有权的转移
先看一下这段代码
- void test_move()
- {
- std::string str = "Hello world.";
- std::vector<std::string> v;
- // 将使用 push_back(const T&), 即产生拷贝行为
- v.push_back(str);
- // 将输出 "str: Hello world."
- std::cout <<"str:" << str << std::endl;
- // 将使用 push_back(const T&&), 不会出现拷贝行为
- // 而整个字符串会被移动到 vector 中, 所以有时候 std::move 会用来减少拷贝出现的开销
- // 这步操作后, str 中的值会变为空
- v.push_back(std::move(str));
- // 将输出 "str:"
- std::cout << "str:" << str << std::endl;
- }
即我们前面提到的"hello world"这些内容的所有权的转移. move 之后, 原先的 str 已经失去了所有权, 打印为空.
再来看一段代码.
- void test_move_efficience()
- {
- vector<int> v_i;
- for(auto i = 0;i<10000000;i++)
- {
- v_i.push_back(i);
- }
- auto f = [&](vector<int> v)
- {
- return v;
- };
- auto g = [&](vector<int>& v)
- {
- return v;
- };
- auto start = std::chrono::system_clock::now();
- f(v_i);
- auto end = std::chrono::system_clock::now();
- std::chrono::duration<double,std::milli> elapsed_seconds = end-start;
- cout <<"f(v_i) elapsed_seconds:"<<elapsed_seconds.count()<<endl;
- start = std::chrono::system_clock::now();
- f(move(v_i));
- end = std::chrono::system_clock::now();
- elapsed_seconds = end-start;
- cout << "f(move(v_i)) elapsed_seconds:"<<elapsed_seconds.count()<<endl;
- start = std::chrono::system_clock::now();
- g(v_i);
- end = std::chrono::system_clock::now();
- elapsed_seconds = end-start;
- cout << "g(v_i) elapsed_seconds:"<<elapsed_seconds.count()<<endl;
- list<int> li;
- for(auto i = 0;i<10000000;i++)
- {
- li.push_front(i);
- li.push_back(i+1);
- }
- vector<list<int>> vl;
- start = std::chrono::system_clock::now();
- vl.push_back(li);
- end = std::chrono::system_clock::now();
- elapsed_seconds = end-start;
- cout << "vl.push_back(li) elapsed_seconds:"<<elapsed_seconds.count()<<endl;
- start = std::chrono::system_clock::now();
- vl.push_back(move(li));
- end = std::chrono::system_clock::now();
- elapsed_seconds = end-start;
- cout << "vl.push_back(move(li)) elapsed_seconds:"<<elapsed_seconds.count()<<endl;
- }
先贴输出
我们知道, 函数传参的时候, 值传递的话, 实际上是做了一份参数的拷贝. 所以f(v_i) 的开销是最高的, 达 15ms,f(move(v_i)) 的话, 没有了拷贝的操作, 耗时1ms. 对于g(v_i) 的话, 参数为引用, 没有拷贝操作, 耗时只有 0.000594ms. 因为不管是 f(),g(), 函数内部都是非常简单的, 没有任何操作, 而 move() 本身会带来一定消耗, 所以f(move(v_i)) 耗时比g(v_i) 更高.
而 move 语义更常见的使用在于标准库里的封装. 比如上述代码, vl.push_back(li) 和 vl.push_back(move(li)) 调用了不同的 push_back(). 其性能是有着数量级的差异. 以我们的例子为例, 其耗时分别为 1207ms 和 0.001548ms.
所以如果我们自己的数据类型, 内部含有大量的数据, 我们应当自己去实现 move 构造函数. 这样使用标准库时才可以更好的提高性能.
来源: https://www.cnblogs.com/sdu20112013/p/11481540.html