c++各版本
C++11 :
1. 在 C++11 之前的版本(C++98 和 C++ 03)中,定义变量或者声明变量之前都必须指明它的类型,比如 int、char 等;但是在一些比较灵活的语言中,比如 C#、JavaScript、PHP、Python 等,程序员在定义变量时可以不指明具体的类型,而是让编译器(或者解释器)自己去推导,这就让代码的编写更加方便。C++11 为了顺应这种趋势也开始支持自动类型推导了!C++11 使用 auto 关键字来支持自动类型推导。
- 我们在使用 stl 容器的时候,需要使用迭代器来遍历容器里面的元素;不同容器的迭代器有不同的类型,在定义迭代器时必须指明。而迭代器的类型有时候比较复杂,书写起来很麻烦,看下面的例子:
1 #include <vector> 2 3 using namespace std; 4 5 int main(){ 6 vector< vector<int> > v; 7 vector< vector<int> >::iterator i = v.begin(); 8 return 0; 9 }
我们大可不必这样,只写一个 auto 即可:
1 #include <vector> 2 3 using namespace std; 4 5 int main(){ 6 vector< vector<int> > v; 7 auto i = v.begin(); //使用 auto 代替具体的类型 8 return 0; 9 }
- auto的另一个应用就是当我们不知道变量是什么类型,或者不希望指明具体类型的时候,比如泛型编程中:
1 #include <iostream> 2 3 using namespace std; 4 5 class A{ 6 public: 7 static int get(void){ 8 return 100; 9 } 10 }; 11 12 class B{ 13 public: 14 static const char* get(void){ 15 return "http://c.biancheng.net/cplus/"; 16 } 17 }; 18 19 template <typename T> 20 void func(void){ 21 auto val = T::get(); 22 cout << val << endl; 23 } 24 25 int main(void){ 26 func<A>(); 27 func<B>(); 28 return 0; 29 }
下面的代码演示了不使用 auto 的解决办法:
1 #include <iostream> 2 3 using namespace std; 4 5 class A{ 6 public: 7 static int get(void){ 8 return 100; 9 } 10 }; 11 12 class B{ 13 public: 14 static const char* get(void){ 15 return "http://c.biancheng.net/cplus/"; 16 } 17 }; 18 19 template <typename T1, typename T2> //额外增加一个模板参数 T2 20 void func(void){ 21 T2 val = T1::get(); 22 cout << val << endl; 23 } 24 25 int main(void){ 26 //调用时也要手动给模板参数赋值 27 func<A, int>(); 28 func<B, const char*>(); 29 return 0; 30 }
2. decltype 是 C++11 新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推导。
既然已经有了 auto 关键字,为什么还需要 decltype 关键字呢?因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚至压根无法使用:
auto varname = value;
decltype(exp) varname = value;
其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。
auto 根据=右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。
另外,auto 要求变量必须初始化,而 decltype 不要求。这很容易理解,auto 是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。decltype可以写成下面的形式:
decltype(exp) varname;
exp 注意事项:原则上讲,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是我们必须要保证 exp 的结果是有类型的,不能是 void;例如,当 exp 调用一个返回值类型为 void 的函数时,exp 的结果也是 void 类型,此时就会导致编译错误。
decltype 用法举例:
int a = 0; decltype(a) b = 1; //b 被推导成了 int decltype(10.8) x = 5.5; //x 被推导成了 double decltype(x + 100) y; //y 被推导成了 double,注意y没有被初始化
当程序员使用 decltype(exp) 获取类型时,编译器将根据以下三条规则得出结果:
- 如果 exp 是一个不被括号( )包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么 decltype(exp) 的类型就和 exp 一致,这是最普遍最常见的情况。
- 如果 exp 是函数调用(注意是函数调用,不是函数名),那么 decltype(exp) 的类型就和函数返回值的类型一致。
- 如果 exp 是一个左值,或者被括号( )包围,那么 decltype(exp) 的类型就是 exp 的引用;假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&。
1 #include <string> 2 3 using namespace std; 4 5 class Student{ 6 public: 7 static int total; 8 string name; 9 int age; 10 float scores; 11 }; 12 13 int Student::total = 0; 14 15 int main(){ 16 int n = 0; 17 const int &r = n; 18 Student stu; 19 decltype(n) a = n; //n 为 int 类型,a 被推导为 int 类型 20 decltype(r) b = n; //r 为 const int& 类型, b 被推导为 const int& 类型 21 decltype(Student::total) c = 0; //total 为类 Student 的一个 int 类型的成员变量,c 被推导为 int 类型 22 decltype(stu.name) url = "http://c.biancheng.net/cplus/"; //total 为类 Student 的一个 string 类型的成员变量, url 被推导为 string 类型 23 return 0; 24 } 25 26 int& func_int_r(int, char); //返回值为 int& 27 28 int&& func_int_rr(void); //返回值为 int&& 29 30 int func_int(double); //返回值为 int 31 32 const int& fun_cint_r(int, int, int); //返回值为 const int& 33 34 const int&& func_cint_rr(void); //返回值为 const int&& 35 36 //decltype类型推导 37 38 int n = 100; 39 40 decltype(func_int_r(100, 'A')) a = n; //a 的类型为 int& 41 42 decltype(func_int_rr()) b = 0; //b 的类型为 int&& 43 44 decltype(func_int(10.5)) c = 0; //c 的类型为 int 45 46 decltype(fun_cint_r(1,2,3)) x = n; //x 的类型为 const int & 47 48 decltype(func_cint_rr()) y = 0; // y 的类型为 const int&& 49 50 /* 需要注意的是,exp 中调用函数时需要带上括号和参数,但这仅仅是形式,并不会真的去执行函数代码 */
auto和decltype的其他区别:
- auto 只能用于类的静态成员,不能用于类的非静态成员(普通成员),如果我们想推导非静态成员的类型,这个时候就必须使用 decltype 了。
1 #include <vector> 2 3 using namespace std; 4 5 template <typename T> 6 class Base { 7 public: 8 void func(T& container) { 9 m_it = container.begin(); 10 } 11 private: 12 typename T::iterator m_it; //注意这里 13 }; 14 15 int main() 16 { 17 const vector<int> v; 18 Base<const vector<int>> obj; 19 obj.func(v); 20 return 0; 21 }
如果传入一个 const 类型的容器,编译器马上就会弹出一大堆错误信息。原因就在于,T::iterator并不能包括所有的迭代器类型,当 T 是一个 const 容器时,应当使用 const_iterator。
要想解决这个问题,在之前的 C++98/03 版本下只能想办法把 const 类型的容器用模板特化单独处理,增加了不少工作量,看起来也非常晦涩。但是有了 C++11 的 decltype 关键字,就可以直接这样写:
1 template <typename T> 3 class Base { 4 public: 5 void func(T& container) { 6 m_it = container.begin(); 7 } 8 9 private: 10 decltype(T().begin()) m_it; //注意这里 11 };
- auto 和 decltype还有一个区别是对 cv 限定符的处理
「cv 限定符」是 const 和 volatile 关键字的统称:
- const 关键字用来表示数据是只读的,也就是不能被修改;
- volatile 和 const 是相反的,它用来表示数据是可变的、易变的,目的是不让 CPU 将数据缓存到寄存器,而是从原始的内存中读取。
在推导变量类型时,auto 和 decltype 对 cv 限制符的处理是不一样的。decltype 会保留 cv 限定符,而 auto 有可能会去掉 cv 限定符。
以下是 auto 关键字对 cv 限定符的推导规则:
- 如果表达式的类型不是指针或者引用,auto 会把 cv 限定符直接抛弃,推导成 non-const 或者 non-volatile 类型。
- 如果表达式的类型是指针或者引用,auto 将保留 cv 限定符。
下面的例子演示了对 const 限定符的推导:
1 //非指针非引用类型 2 const int n1 = 0; 3 auto n2 = 10; 4 n2 = 99; //赋值不报错 5 decltype(n1) n3 = 20; 6 n3 = 5; //赋值报错 7 8 //指针类型 9 const int *p1 = &n1; 10 auto p2 = p1; 11 *p2 = 66; //赋值报错 12 decltype(p1) p3 = p1; 13 *p3 = 19; //赋值报错
当表达式的类型为引用时,auto 和 decltype 的推导规则也不一样;decltype 会保留引用类型,而 auto 会抛弃引用类型,直接推导出它的原始类型。例如:
1 #include <iostream> 2 3 using namespace std; 4 5 int main() { 6 int n = 10; 7 int &r1 = n; 8 //auto推导 9 auto r2 = r1; 10 r2 = 20; 11 cout << n << ", " << r1 << ", " << r2 << endl; 12 //decltype推导 13 decltype(r1) r3 = n; 14 r3 = 99; 15 cout << n << ", " << r1 << ", " << r3 << endl; 16 return 0; 17 } 18 19 /*运行结果: 20 10, 10, 20 21 99, 99, 99*/
总结
auto 虽然在书写格式上比 decltype 简单,但是它的推导规则复杂,有时候会改变表达式的原始类型;而 decltype 比较纯粹,它一般会坚持保留原始表达式的任何类型,让推导的结果更加原汁原味。
从代码是否健壮的角度考虑,我推荐使用 decltype,它没有那么多是非;但是 decltype 总是显得比较麻烦,尤其是当表达式比较复杂时,例如:
vector nums;
decltype(nums.begin()) it = nums.begin();
而如果使用 auto 就会清爽很多:
vector nums;
auto it = nums.begin();
在实际开发中人们仍然喜欢使用 auto 关键字(我也这么干),因为它用起来简单直观,更符合人们的审美。如果你的表达式类型不复杂,我还是推荐使用 auto 关键字,优雅的代码总是叫人赏心悦目,沉浸其中。
C++返回值类型后置(跟踪返回值类型)
在泛型编程中,可能需要通过参数的运算来得到返回值的类型。考虑下面这个场景:
1 template <typename R, typename T, typename U> 2 R add(T t, U u) 3 { 4 return t+u; 5 } 6 7 int a = 1; float b = 2.0; 8 auto c = add<decltype(a + b)>(a, b);
我们并不关心 a+b 的类型是什么,因此,只需要通过 decltype(a+b) 直接得到返回值类型即可。但是像上面这样使用十分不方便,因为外部其实并不知道参数之间应该如何运算,只有 add 函数才知道返回值应当如何推导。
那么,在 add 函数的定义上能不能直接通过 decltype 拿到返回值呢?
template <typename T, typename U> decltype(t + u) add(T t, U u) // error: t、u尚未定义 { return t + u; }
当然,直接像上面这样写是编译不过的。因为 t、u 在参数列表中,而 C++ 的返回值是前置语法,在返回值定义的时候参数变量还不存在。
可行的写法如下:
template <typename T, typename U> decltype(T() + U()) add(T t, U u) { return t + u; }
考虑到 T、U 可能是没有无参构造函数的类,正确的写法应该是这样:
template <typename T, typename U> decltype((*(T*)0) + (*(U*)0)) add(T t, U u) { return t + u; }
虽然成功地使用 decltype 完成了返回值的推导,但写法过于晦涩,会大大增加 decltype 在返回值类型推导上的使用难度并降低代码的可读性。
3. 因此,在 C++11 中增加了返回类型后置(trailing-return-type,又称跟踪返回类型)语法,将 decltype 和 auto 结合起来完成返回值类型的推导。
返回类型后置语法是通过 auto 和 decltype 结合起来使用的。上面的 add 函数,使用新的语法可以写成:
template <typename T, typename U> auto add(T t, U u) -> decltype(t + u) { return t + u; }
另一个例子:
int& foo(int& i); float foo(float& f); template <typename T> auto func(T& val) -> decltype(foo(val)) { return foo(val); }
返回值类型后置语法,是为了解决函数返回值类型依赖于参数而导致难以确定返回值类型的问题。有了这种语法以后,对返回值类型的推导就可以用清晰的方式(直接通过参数做运算)描述出来,而不需要像 C++98/03 那样使用晦涩难懂的写法。
4. C++11支持连续两个右尖括号,但C++98/03 中不支持:
template <typename T> struct Foo { typedef T type; }; template <typename T> class A { // ... }; int main(void) { Foo<A<int>>::type xx; //编译报错:error: ‘>>’ should be ‘>>’ within a nested template argument list Foo<A>::type xx; return 0; }
意思就是,Foo<A<int>>这种写法是不被支持的,要写成这样Foo<A<int> >(注意两个右尖括号之间的空格)
5. c++11支持使用using new_name = old_name的语法来起别名
在此之前,使用 typedef 重定义类型是很方便的,但它也有一些限制,比如,无法重定义一个模板:
typedef std::map<std::string, int> map_int_t; // … typedef std::map<std::string, std::string> map_str_t; // …
我们需要的其实是一个固定以 std::string 为 key 的 map,它可以映射到 int 或另一个 std::string。然而这个简单的需求仅通过 typedef 却很难办到。
因此,在 C++98/03 中往往不得不这样写:
template <typename Val> struct str_map { typedef std::map<std::string, Val> type; }; // ... str_map<int>::type map1; // ...
现在,在 C++11 中终于出现了可以重定义一个模板的语法。请看下面的示例:
template <typename Val> using str_map_t = std::map<std::string, Val>; // ... str_map_t<int> map1;
实际上,using 的别名语法覆盖了 typedef 的全部功能。
先来看看对普通类型的重定义示例:
// 重定义unsigned int typedef unsigned int uint_t; using uint_t = unsigned int; // 重定义std::map typedef std::map<std::string, int> map_int_t; using map_int_t = std::map<std::string, int>;
可以看到,在重定义普通类型上,两种使用方法的效果是等价的,唯一不同的是定义语法。
typedef 的定义方法和变量的声明类似:像声明一个变量一样,声明一个重定义类型,之后在声明之前加上 typedef 即可。这种写法凸显了 C/C++ 中的语法一致性,但有时却会增加代码的阅读难度。
比如重定义一个函数指针时:
typedef void (*func_t)(int, int);
与之相比,using 后面总是立即跟随新标识符(Identifier),之后使用类似赋值的语法,把现有的类型(type-id)赋给新类型:
using func_t = void (*)(int, int);
从上面的对比中可以发现,C++11 的 using 别名语法比 typedef 更加清晰。因为 typedef 的别名语法本质上类似一种解方程的思路。而 using 语法通过赋值来定义别名,和我们平时的思考方式一致。
下面再通过一个对比示例,看看新的 using 语法是如何定义模板别名的。
/* C++98/03 */ template <typename T> struct func_t { typedef void (*type)(T, T); }; // 使用 func_t 模板 func_t<int>::type xx_1; /* C++11 */ template <typename T> using func_t = void (*)(T, T); // 使用 func_t 模板 func_t<int> xx_2;
注意,using 重定义的 func_t 是一个模板,但它既不是类模板也不是函数模板(函数模板实例化后是一个函数),而是一种新的模板形式:模板别名(alias template)。
6. C++11支持类模板和函数模板的默认模板参数(c++98只支持类模板的默认模板参数)
7. C++11在函数模板和类模板中使用可变参数
8. C++11 标准新引入了一种类模板,命名为 tuple(中文可直译为元组)。tuple 最大的特点是:实例化的对象可以存储任意数量、任意类型的数据。
9. C++11采用列表初始化(统一了初始化方式)
我们知道,在 C++98/03 中的对象初始化方法有很多种,请看下面的代码:
//初始化列表 int i_arr[3] = { 1, 2, 3 }; //普通数组 struct A { int x; struct B { int i; int j; } b; } a = { 1, { 2, 3 } }; //POD类型 //拷贝初始化(copy-initialization) int i = 0; class Foo { public: Foo(int) {} } foo = 123; //需要拷贝构造函数 //直接初始化(direct-initialization) int j(0); Foo bar(123);
这些种类繁多的初始化方法,没有一种可以通用所有情况。为了统一初始化方式,并且让初始化行为具有确定的效果,C++11 中提出了列表初始化(List-initialization)的概念。
POD 类型即 plain old data 类型(具体定义见下文),简单来说,是可以直接使用 memcpy 复制的对象。
统一的初始化
在上面我们已经看到了,对于普通数组和 POD 类型,C++98/03 可以使用初始化列表(initializer list)进行初始化:
int i_arr[3] = { 1, 2, 3 }; long l_arr[] = { 1, 3, 2, 4 }; struct A { int x; int y; } a = { 1, 2 };
但是这种初始化方式的适用性非常狭窄,只有上面提到的这两种数据类型可以使用初始化列表。
在 C++11 中,初始化列表的适用性被大大增加了,可以用于任何类型对象的初始化:
//通过初始化列表初始化对象 class Foo { public: Foo(int) {} private: Foo(const Foo &); }; int main(void) { Foo a1(123); Foo a2 = 123; //error: 'Foo::Foo(const Foo &)' is private Foo a3 = { 123 }; Foo a4 { 123 }; int a5 = { 3 }; int a6 { 3 }; return 0; }
a3、a4 使用了新的初始化方式来初始化对象,效果如同 a1 的直接初始化。
a5、a6 则是基本数据类型的列表初始化方式。可以看到,它们的形式都是统一的。
注意a3 虽然使用了等于号,但它仍然是列表初始化,因此,私有的拷贝构造并不会影响到它。a4 和 a6 的写法,是 C++98/03 所不具备的。这种变量名后面跟上初始化列表方法同样适用于普通数组和 POD 类型的初始化:
int i_arr[3] { 1, 2, 3 }; //普通数组 struct A { int x; struct B { int i; int j; } b; } a { 1, { 2, 3 } }; //POD类型
在初始化时,{}前面的等于号是否书写对初始化行为没有影响。
另外,如同读者所想的那样,new 操作符等可以用圆括号进行初始化的地方,也可以使用初始化列表:
int* a = new int { 123 }; double b = double { 12.12 }; int* arr = new int[3] { 1, 2, 3 };
指针 a 指向了一个 new 操作符返回的内存,通过初始化列表方式在内存初始化时指定了值为 123。
b 则是对匿名对象使用列表初始化后,再进行拷贝初始化。
这里让人眼前一亮的是 arr 的初始化方式。堆上动态分配的数组终于也可以使用初始化列表进行初始化了。
除了上面所述的内容之外,列表初始化还可以直接使用在函数的返回值上:
struct Foo { Foo(int, double) {} }; Foo func(void) { return { 123, 321.0 }; }
10. C++11 新增lambda匿名函数
11. C++11非受限联合体(union)
在 C/C++ 中,联合体(Union)是一种构造数据类型。在一个联合体内,我们可以定义多个不同类型的成员,这些成员将会共享同一块内存空间。老版本的 C++ 为了和C语言保持兼容,对联合体的数据成员的类型进行了很大程度的限制,这些限制在今天看来并没有必要,因此 C++11 取消了这些限制。
C++11 标准规定,任何非引用类型都可以成为联合体的数据成员,这种联合体也被称为非受限联合体。例如:
class Student{ public: Student(bool g, int a): gender(g), age(a) {} private: bool gender; int age; }; union T{ Student s; // 含有非POD类型的成员,gcc-5.1.0 版本报错 char name[10]; }; int main(){ return 0; }
POD 类型一般具有以下几种特征(包括 class、union 和 struct等):
- 没有用户自定义的构造函数、析构函数、拷贝构造函数和移动构造函数。
- 不能包含虚函数和虚基类。
- 非静态成员必须声明为 public。
- 类中的第一个非静态成员的类型与其基类不同,例如:
class B1{}; class B2 : B1 { B1 b; };
class B2 的第一个非静态成员 b 是基类类型,所以它不是 POD 类型。
在类或者结构体继承时,满足以下两种情况之一:
- 派生类中有非静态成员,且只有一个仅包含静态成员的基类;
- 基类有非静态成员,而派生类没有非静态成员。
(简而言之就是基类和派生类中能其中一个有非静态成员,这样的才是POD类型)
我们来看具体的例子:
class B1 { static int n; }; class B2 : B1 { int n1; }; class B3 : B2 { static int n2; };
对于 B2,派生类 B2 中有非静态成员,且只有一个仅包含静态成员的基类 B1,所以它是 POD 类型。对于 B3,基类 B2 有非静态成员,而派生类 B3 没有非静态成员,所以它也是 POD 类型。
所有非静态数据成员均和其基类也符合上述规则(递归定义),也就是说 POD 类型不能包含非 POD 类型的数据。
此外,所有兼容C语言的数据类型都是 POD 类型(struct、union 等不能违背上述规则)。
c++11具体对联合体做了以下改进(改进后称为非受限联合体):
(1) C++11 允许联合体的成员是非 POD 类型
(2) C++11 允许联合体有静态成员:
union U { static int func() { int n = 3; return n; } };
需要注意的是,静态成员变量只能在联合体内定义,却不能在联合体外使用,这使得该规则很没用。
非受限联合体的赋值注意事项:
C++11 规定,如果非受限联合体内有一个非 POD 的成员,而该成员拥有自定义的构造函数,那么这个非受限联合体的默认构造函数将被编译器删除;其他的特殊成员函数,例如默认拷贝构造函数、拷贝赋值操作符以及析构函数等,也将被删除。
这条规则可能导致对象构造失败:
union U { string s; int n; }; int main() { U u; // 构造失败,因为 U 的构造函数被删除(因为 string 类拥有自定义的构造函数) return 0; }
解决上面问题的一般需要用到 placement new(稍后会讲解这个概念),代码如下:
#include <string> using namespace std; union U { string s; int n; public: U() { new(&s) string; } ~U() { s.~string(); } }; int main() { U u; return 0; }
构造时,采用 placement new 将 s 构造在其地址 &s 上,这里 placement new 的唯一作用只是调用了一下 string 类的构造函数。注意,在析构时还需要调用 string 类的析构函数。
placement new 是什么?
placement new 是 new 关键字的一种进阶用法,既可以在栈(stack)上生成对象,也可以在堆(heap)上生成对象。相对应地,我们把常见的 new 的用法称为 operator new,它只能在 heap 上生成对象。
placement new 的语法格式如下:
new(address) ClassConstruct(…)
address 表示已有内存的地址,该内存可以在栈上,也可以在堆上;ClassConstruct(…) 表示调用类的构造函数,如果构造函数没有参数,也可以省略括号。
placement new 利用已经申请好的内存来生成对象,它不再为对象分配新的内存,而是将对象数据放在 address 指定的内存中。在本例中,placement new 使用的是 s 的内存空间。
非受限联合体的匿名声明和“枚举式类”
匿名联合体是指不具名的联合体(也即没有名字的联合体),一般定义如下:
union U{ union { int x; }; //此联合体为匿名联合体 };
可以看到,联合体 U 内定义了一个不具名的联合体,该联合体包含一个 int 类型的成员变量,我们称这个联合体为匿名联合体。
同样的,非受限联合体也可以匿名,而当非受限的匿名联合体运用于类的声明时,这样的类被称为“枚举式类”。示例如下:
#include <cstring> using namespace std; class Student{ public: Student(bool g, int a): gender(g), age(a){} bool gender; int age; }; class Singer { public: enum Type { STUDENT, NATIVE, FOREIGENR }; Singer(bool g, int a) : s(g, a) { t = STUDENT; } Singer(int i) : id(i) { t = NATIVE; } Singer(const char* n, int s) { int size = (s > 9) ? 9 : s; memcpy(name , n, size); name[s] = '\0'; t = FOREIGENR; } ~Singer(){} private: Type t; union { Student s; int id; char name[10]; }; }; int main() { Singer(true, 13); Singer(310217); Singer("J Michael", 9); return 0; }
上面的代码中使用了一个匿名非受限联合体,它作为类 Singer 的“变长成员”来使用,这样的变长成员给类的编写带来了更大的灵活性,这是 C++98 标准中无法达到的(编译器会报member 'Student Singer::<anonymous union>::s' with constructor not allowed in union错误)。
12. c++11引入范围for循环
13. C++11 引入constexpr 关键字
14. C++11 引入 long long 类型
15. C++11 引入右值引用
16. C++11 引入 move() 函数
17. C++11 引入nullptr 常量
18. C++11 引入shared_ptr智能指针
c++14
1. 新增函数返回值类型推导:
#include <iostream> using namespace std; auto func(int i) { return i; } int main() { cout << func(4) << endl; return 0; } /* c++11下编译会报错 */
返回值类型推导也可以用在模板中:
#include <iostream> using namespace std; template<typename T> auto func(T t) { return t; } int main() { cout << func(4) << endl; cout << func(3.4) << endl; return 0; }
注意:
- 函数内如果有多个return语句,它们必须返回相同的类型,否则编译失败。
- 如果return语句返回初始化列表,返回值类型推导会失败:
auto func() { return {1, 2, 3}; // error returning initializer list }
- 如果函数是虚函数,不能使用返回值类型推导:
struct A { // error: virtual function cannot have deduced return type virtual auto func() { return 1; } }
- 返回类型推导可以用在前向声明中,但是在使用它们之前,翻译单元中必须能够得到函数定义:
auto f(); // declared, not yet defined auto f() { return 42; } // defined, return type is int int main() { cout << f() << endl; }
- 在C++11中,lambda表达式参数需要使用具体的类型声明:
auto f = [] (int a) { return a; }
// 但是在C++14中,对此进行优化,lambda表达式参数可以直接是auto: auto f = [] (auto a) { return a; }; cout << f(1) << endl; cout << f(2.3f) << endl;
2. C++14支持变量模板:
template<class T> constexpr T pi = T(3.1415926535897932385L); int main() { cout << pi<int> << endl; // 3 cout << pi<double> << endl; // 3.14159 return 0; }
3. C++14支持别名模板:
template<typename T, typename U> struct A { T t; U u; }; template<typename T> using B = A<T, int>; int main() { B<double> b; b.t = 10; b.u = 20; cout << b.t << endl; cout << b.u << endl; return 0; }
4. C++14相较于C++11对constexpr减少了一些限制:
constexpr int factorial(int n) { // C++14 和 C++11均可 return n <= 1 ? 1 : (n * factorial(n - 1)); }
在C++14中可以这样做:
constexpr int factorial(int n) { // C++11中不可,C++14中可以 int ret = 0; for (int i = 0; i < n; ++i) { ret += i; } return ret; }
C++11中constexpr函数必须必须把所有东西都放在一个单独的return语句中,而constexpr则无此限制:
constexpr int func(bool flag) { // C++14 和 C++11均可 return 0; }
在C++14中可以这样:
constexpr int func(bool flag) { // C++11中不可,C++14中可以 if (flag) return 1; else return 0; }
5. C++14引入了二进制字面量,也引入了分隔符,防止看起来眼花哈~
int a = 0b0001'0011'1010; double b = 3.14'1234'1234'1234;
6. 我们都知道C++11中有std::make_shared,却没有std::make_unique,在C++14已经改善:
struct A {}; std::unique_ptr<A> ptr = std::make_unique<A>();
C++14通过std::shared_timed_mutex和std::shared_lock来实现读写锁,保证多个线程可以同时读,但是写线程必须独立运行,写操作不可以同时和读操作一起进行:
struct ThreadSafe { mutable std::shared_timed_mutex mutex_; int value_; ThreadSafe() { value_ = 0; } int get() const { std::shared_lock<std::shared_timed_mutex> loc(mutex_); return value_; } void increase() { std::unique_lock<std::shared_timed_mutex> lock(mutex_); value_ += 1; } };
为什么是timed的锁呢,因为可以带超时时间,具体可以自行查询相关资料哈,网上有很多。
std::integer_sequence template<typename T, T... ints> void print_sequence(std::integer_sequence<T, ints...> int_seq) { std::cout << "The sequence of size " << int_seq.size() << ": "; ((std::cout << ints << ' '), ...); std::cout << '\n'; } int main() { print_sequence(std::integer_sequence<int, 9, 2, 5, 1, 9, 1, 6>{}); return 0; } //输出:7 9 2 5 1 9 1 6
std::integer_sequence和std::tuple的配合使用:
template <std::size_t... Is, typename F, typename T> auto map_filter_tuple(F f, T& t) { return std::make_tuple(f(std::get<Is>(t))...); } template <std::size_t... Is, typename F, typename T> auto map_filter_tuple(std::index_sequence<Is...>, F f, T& t) { return std::make_tuple(f(std::get<Is>(t))...); } template <typename S, typename F, typename T> auto map_filter_tuple(F&& f, T& t) { return map_filter_tuple(S{}, std::forward<F>(f), t); }
7. c++14新增 std::exchange:
int main() { std::vector<int> v; std::exchange(v, {1,2,3,4}); cout << v.size() << endl; for (int a : v) { cout << a << " "; } return 0; }
看样子貌似和std::swap作用相同,那它俩有什么区别呢?
可以看下exchange的实现:
template<class T, class U = T> constexpr T exchange(T& obj, U&& new_value) { T old_value = std::move(obj); obj = std::forward<U>(new_value); return old_value; }
可以看见new_value的值给了obj,而没有对new_value赋值,这里相信您已经知道了它和swap的区别了吧!
8. C++14引入std::quoted用于给字符串添加双引号:
int main() { string str = "hello world"; cout << str << endl; cout << std::quoted(str) << endl; return 0; } /* 结果: hello world "hello world" */
c++17
1. 构造函数模板推导
在C++17前构造一个模板类对象需要指明类型:
pair<int, double> p(1, 2.2); // before c++17
C++17就不需要特殊指定,直接可以推导出类型,代码如下:
pair p(1, 2.2); // c++17 自动推导 vector v = {1, 2, 3}; // c++17
2. 结构化绑定
通过结构化绑定,对于tuple、map等类型,获取相应值会方便很多:
std::tuple<int, double> func() { return std::tuple(1, 2.2); } int main() { auto[i, d] = func(); //是C++11的tie吗?更高级 cout << i << endl; cout << d << endl; } //========================== void f() { map<int, string> m = { {0, "a"}, {1, "b"}, }; for (const auto &[i, s] : m) { cout << i << " " << s << endl; } } // ==================== int main() { std::pair a(1, 2.3f); auto[i, f] = a; cout << i << endl; // 1 cout << f << endl; // 2.3f return 0; }
结构化绑定还可以改变对象的值,使用引用即可:
// 进化,可以通过结构化绑定改变对象的值 int main() { std::pair a(1, 2.3f); auto& [i, f] = a; i = 2; cout << a.first << endl; // 2 }
注意结构化绑定不能应用于constexpr
constexpr auto[x, y] = std::pair(1, 2.3f); // compile error(但是C++20可以)
结构化绑定不止可以绑定pair和tuple,还可以绑定数组和结构体等
int array[3] = {1, 2, 3}; auto [a, b, c] = array; cout << a << " " << b << " " << c << endl; // 注意这里的struct的成员一定要是public的 struct Point { int x; int y; }; Point func() { return {1, 2}; } const auto [x, y] = func();
这里其实可以实现自定义类的结构化绑定,代码如下:
// 需要实现相关的tuple_size和tuple_element和get<N>方法。 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; // name 10 return 0; }
3. if-switch语句初始化
C++17前if语句需要这样写代码:
int a = GetValue(); if (a < 101) { cout << a; }
C++17之后可以这样:
// if (init; condition) if (int a = GetValue()); a < 101) { cout << a; } string str = "Hi World"; if (auto [pos, size] = pair(str.find("Hi"), str.size()); pos != string::npos) { std::cout << pos << " Hello, size is " << size; }
4. C++17前只有内联函数,现在有了内联变量,我们印象中C++类的静态成员变量在头文件中是不能初始化的,但是有了内联变量,就可以达到此目的:
// header file struct A { static const int value; }; inline int const A::value = 10; // ==========或者======== struct A { inline static const int value = 10; }
5. C++17引入了折叠表达式使可变参数模板编程更方便:
template <typename ... Ts> auto sum(Ts ... ts) { return (ts + ...); } int a {sum(1, 2, 3, 4, 5)}; // 15 std::string a{"hello "}; std::string b{"world"}; cout << sum(a, b) << endl; // hello world
6. C++17前lambda表达式只能在运行时使用,C++17引入了constexpr lambda表达式,可以用于在编译期进行计算。
int main() { // c++17可编译 constexpr auto lamb = [] (int n) { return n * n; }; static_assert(lamb(3) == 9, "a"); }
注意:constexpr函数有如下限制:
函数体不能包含汇编语句、goto语句、label、try块、静态变量、线程局部存储、没有初始化的普通变量,不能动态分配内存,不能有new delete等,不能虚函数。
7. c++17优化namespace嵌套:
namespace A { namespace B { namespace C { void func(); } } } // c++17,更方便更舒适 namespace A::B::C { void func();) }
8. __has_include预处理表达式
可以判断是否有某个头文件,代码可能会在不同编译器下工作,不同编译器的可用头文件有可能不同,所以可以使用此来判断:
#if defined __has_include #if __has_include(<charconv>) #define has_charconv 1 #include <charconv> #endif #endif std::optional<int> ConvertToInt(const std::string& str) { int value{}; #ifdef has_charconv const auto last = str.data() + str.size(); const auto res = std::from_chars(str.data(), last, value); if (res.ec == std::errc{} && res.ptr == last) return value; #else // alternative implementation... //其它方式实现 #endif return std::nullopt; }
9. 在lambda表达式用*this捕获对象副本
正常情况下,lambda表达式中访问类的对象成员变量需要捕获this,但是这里捕获的是this指针,指向的是对象的引用,正常情况下可能没问题,但是如果多线程情况下,函数的作用域超过了对象的作用域,对象已经被析构了,还访问了成员变量,就会有问题。
struct A { int a; void func() { auto f = [this] { cout << a << endl; }; f(); } }; int main() { A a; a.func(); return 0; }
所以C++17增加了新特性,捕获*this,不持有this指针,而是持有对象的拷贝,这样生命周期就与对象的生命周期不相关啦。
struct A { int a; void func() { auto f = [*this] { // 这里 cout << a << endl; }; f(); } }; int main() { A a; a.func(); return 0; }
10. 新增Attribute
我们可能平时在项目中见过__declspec, attribute , #pragma指示符,使用它们来给编译器提供一些额外的信息,来产生一些优化或特定的代码,也可以给其它开发者一些提示信息。例如:
struct A { short f[3]; } __attribute__((aligned(8))); void fatal() __attribute__((noreturn));
在C++11和C++14中有更方便的方法:
[[carries_dependency]] 让编译期跳过不必要的内存栅栏指令
[[noreturn]] 函数不会返回
[[deprecated]] 函数将弃用的警告
[[noreturn]] void terminate() noexcept;
[[deprecated("use new func instead")]] void func() {}
C++17又新增了三个:
[[fallthrough]],用在switch中提示可以直接落下去,不需要break,让编译期忽略警告
switch (i) {} case 1: xxx; // warning case 2: xxx; [[fallthrough]]; // 警告消除 case 3: xxx; break; }
使得编译器和其它开发者都可以理解开发者的意图。
[[nodiscard]] :表示修饰的内容不能被忽略,可用于修饰函数,标明返回值一定要被处理
[[nodiscard]] int func(); void F() { func(); // warning 没有处理函数返回值 }
[[maybe_unused]] :提示编译器修饰的内容可能暂时没有使用,避免产生警告
void func1() {} [[maybe_unused]] void func2() {} // 警告消除 void func3() { int x = 1; [[maybe_unused]] int y = 2; // 警告消除 }
11. 字符串转换新增from_chars函数和to_chars函数:
#include <charconv> int main() { const std::string str{"123456098"}; int value = 0; const auto res = std::from_chars(str.data(), str.data() + 4, value); if (res.ec == std::errc()) { cout << value << ", distance " << res.ptr - str.data() << endl; } else if (res.ec == std::errc::invalid_argument) { cout << "invalid" << endl; } str = std::string("12.34); double val = 0; const auto format = std::chars_format::general; res = std::from_chars(str.data(), str.data() + str.size(), value, format); str = std::string("xxxxxxxx"); const int v = 1234; res = std::to_chars(str.data(), str.data() + str.size(), v); cout << str << ", filled " << res.ptr - str.data() << " characters \n"; // 1234xxxx, filled 4 characters }
12. 新增std::variant实现类似union的功能,但却比union更高级,举个例子union里面不能有string这种类型,但std::variant却可以,还可以支持更多复杂类型,如map等:
int main() { // c++17可编译 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); // 通过index获取对应值 cout << str << endl; cout << i << endl; } catch(...) { // xxx; } return 0; }
注意:一般情况下variant的第一个类型一般要有对应的构造函数,否则编译失败:
struct A { A(int i){} }; int main() { std::variant<A, int> var; // 编译失败 }
如何避免这种情况呢,可以使用std::monostate来打个桩,模拟一个空状态。
std::variant<std::monostate, A> var; // 可以编译成功
13. 新增std::optional。我们有时候可能会有需求,让函数返回一个对象,如下:
struct A {}; A func() { if (flag) return A(); else { // 异常情况下,怎么返回异常值呢,想返回个空呢 } }
有一种办法是返回对象指针,异常情况下就可以返回nullptr啦,但是这就涉及到了内存管理,也许你会使用智能指针,但这里其实有更方便的办法就是std::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; } }
14. 新增std::any
C++17引入了any可以存储任何类型的单个值,见代码:
int main() { // c++17可编译 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; }
15. 新增std::apply
使用std::apply可以将tuple展开作为函数的参数传入,见代码:
int add(int first, int second) { return first + second; } auto add_lambda = [](auto first, auto second) { return first + second; }; int main() { std::cout << std::apply(add, std::pair(1, 2)) << '\n'; std::cout << add(std::pair(1, 2)) << "\n"; // error std::cout << std::apply(add_lambda, std::tuple(2.0f, 3.0f)) << '\n'; }
16. 新增std::make_from_tuple
使用make_from_tuple可以将tuple展开作为构造函数参数
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)); }
17. 新增std::string_view
通常我们传递一个string时会触发对象的拷贝操作,大字符串的拷贝赋值操作会触发堆内存分配,很影响运行效率,有了string_view就可以避免拷贝操作,平时传递过程中传递string_view即可。
void func(std::string_view stv) { cout << stv << endl; } int main(void) { std::string str = "Hello World"; std::cout << str << std::endl; std::string_view stv(str.c_str(), str.size()); cout << stv << endl; func(stv); return 0; }
18. C++17使用as_const可以将左值转成const类型
std::string str = "str"; const std::string& constStr = std::as_const(str);
19. C++17正式将file_system纳入标准中,提供了关于文件的大多数功能,基本上应有尽有,这里简单举几个例子:
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);
20. C++17引入了shared_mutex,可以实现读写锁
c++20
1. 新增关键字(keywords)
concept
requires
constinit
consteval
co_await
co_return
co_yield
char8_t
2. 新增标识符(Identifies)
import
module
3. 新增模块(Modules)
优点
没有头文件
声明实现仍然可分离, 但非必要
可以显式指定那些导出(类, 函数等)
不需要头文件重复引入宏 (include guards)
模块之间名称可以相同不会冲突
模块只处理一次, 编译更快 (头文件每次引入都需要处理)
预处理宏只在模块内有效
模块引入顺序无关紧要
创建模块
// cppcon.cpp
export module cppcon;
namespace CppCon {
auto GetWelcomeHelper() { return "Welcome to CppCon 2019!"; }
export auto GetWelcome() { return GetWelcomeHelper();}
}
引用模块
// main.cpp
import cppcon;
int main(){
std::cout << CppCon::GetWelcome();
}
import 头文件
import
隐式地将 iostream 转换为模块
加速构建, 因为 iostream 只会处理一次
和预编译头 (PCH) 具有相似的效果
Ranges:代表一串元素, 或者一串元素中的一段,类似 begin/end 对
好处:简化语法和方便使用,防止 begin/end 不配对,使变换/过滤等串联操作成为可能
vector<int> data{11, 22, 33};
sort(begin(data), end(data));
sort(data); // 使用 Ranges
C++20 新增特性总结
新增关键字(keywords)
新增标识符(Identifies)
模块(Modules)
优点
创建模块
引用模块
import 头文件
Ranges
例子
协程(Coroutines)
什么是协程
例子(VC++)
Concepts
如何定义
使用
例子
Lambda 表达式的更新
[=, this] 需要显式捕获this变量
模板形式的 Lambda 表达式
Lambda 表达式打包捕获(Pack Expansion)
常量表达式(constexpr) 的更新
constexpr string & vector
原子(Atomic)智能指针
例子
自动合流(Joining), 可中断(Cancellable) 的线程
例子
C++20 同步(Synchronization)库
std::atomic_ref
其他更新
指定初始化(Designated Initializers)
航天飞机操作符 <=>
范围 for 循环语句支持初始化
非类型模板形参支持字符串
[[likely]], [[unlikely]]
日历(Calendar)和时区(Timezone)功能
std::span
特性测试宏
consteval 函数
constinit
用 using 引用 enum 类型
格式化库(std::format)
增加数学常量
std::source_location
[[nodiscard(reason)]]
位运算