C++11特性梳理
- 1. 列表初始化
- 2. auto & decltype
- 3. 右值引用
- 3.1. 左右值引用比较
- 3.2. 右值引用的意义
- 3.3. 万能引用与完美转发
- 3.4. 移动构造与移动赋值
- 4. default & delete
- 5. 可变参数模板
- 6. push_back 与 emplace_back
- 7. lambda表达式
- 7.1. 捕捉列表
- 8. function包装器
- 8.1. bind
C++11是继C++98之后,第二个真正意义上的标准。C++11增加的语法特性非常多,这里主要介绍实际比较实用的语法。
可以从C++官网对C++11版本特性进行全面的了解:https://en.cppreference.com/w/cpp/11
1. 列表初始化
C++11中的{}
不仅可以用来初始化数组,已经扩展到可以用来初始化C++11的一切对象,而且使用时等号(=)可有可无。
void Test1()
{
int a1 = { 1 };
int a2{ 2 };
vector<int> v1 = { 1,2,3,4,5 };
vector<int> v2{ 1,2,3,4,5 };
map<int, string> m1 = { {1, "one"}, {2, "two"} };
map<int, string> m2{ {1, "one"}, {2, "two"} };
// 也可以用在new表达式中
int* p = new int[4]{ 0 };
}
注意一点:列表初始化,如果出现类型截断,是会报警告或者错误的。short c = 65535;
short d { 65535 }; // err
STL中的很多容器能支持{}
的初始化,其实是因为增加了std::initializer_list
作为构造函数参数的设计,这样使容器的初始化更方便了。operator=
同理。
std::initializer_list
又是什么类型呢?
void Test2()
{
auto il = { 1,2,3 };
cout << typeid(il).name() << endl;
}
可以看到,{}
对象其实就是std::initializer_list
类型的对象。
这里建议,如果在使用容器时有大括号{}
初始化的需求,可以选择使用;其它地方尽量不要使用,来保持语言风格的一致性。
2. auto & decltype
auto
在C++98中是一个存储类型的说明符,但实质上没有什么价值。所以C++11中,auto
就弃用了之前的用法,将其用于实现自动类型推导。auto
此时仅仅只是占位符,编译阶段编译器根据初始化表达式推演出实际类型之后会替换auto
。
void Test3()
{
int i = 10;
auto p = i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
}
decltype
关键字可以将对象的类型声明为表达式生成的类型。
void Test4()
{
char c = 'a';
int i = 1;
decltype(c * i) d1;
decltype(strcpy) d2;
cout << typeid(d1).name() << endl;
cout << typeid(d2).name() << endl;
}
3. 右值引用
有右值引用,自然也有左值引用。其实C++11之前所学的引用都叫做左值引用。但无论左值引用还是右值引用,都是给对象取别名。
对于左值,我们可以取它的地址+对它赋值。但经过const
修饰的左值可以取它的地址,却不能对它赋值。
对于右值,是不能取其地址的。
左值可以出现在赋值符号左边,右值可以出现在赋值符号右边,但反过来就不行。
void Test5()
{
int i = 1;
int* pi = &i;
// 左值引用,给左值取别名
int& ri = i; // 变量
int*& rpi = pi; // 变量
int& r = *pi; // 指针解引用
double d = 2.2;
// 右值引用,给右值取别名
int&& rr1 = 10; // 字面常量
double&& rr2 = i + d; // 表达式返回值
}
虽然右值不能被取地址,但右值引用后,会导致右值被存储到特定位置,从而可以取到该位置的地址。
void Test6()
{
int&& rr1 = 10; // 右值引用
cout << rr1 << endl;
cout << &rr1 << endl; // 可以取地址
rr1 = 20; // 可以赋值
cout << rr1 << endl;
cout << &rr1 << endl; // 可以取地址
}
3.1. 左右值引用比较
左值引用只能引用左值,不能引用右值;但const左值引用既可以引用左值,也可以引用右值。
右值引用只能引用右值,不能引用左值;但右值引用可以引用move
以后的左值。
void Test7()
{
int i = 10;
int& r1 = i;
//int& r2 = 10; // err
const int& r3 = i;
const int& r4 = 10;
//int&& r5 = i; // err
int&& r6 = 10;
int&& r7 = move(i);
}
3.2. 右值引用的意义
左值引用既可以引用左值,又可以通过const
修饰引用右值,那为什么还要提出右值引用呢?
右值引用的加入其实是对左值引用一些缺陷上的补足。
比如说,stack
的pop
接口返回值是void
(void pop();
)。但是如果要将返回值进行修改,使pop
的同时返回其弹出的元素,那该如何设计其返回值类型呢?
如果设计成传值返回,当返回值需要深拷贝时,就会对效率造成影响。
如果设计成传引用返回,如果是左值引用,当出了函数作用域,返回的对象就不存在了,即引用也失效了。
此时,只能设计成右值引用返回。设计成右值引用,把将要被销毁的对象的资源转移出来,可以继续使用,而且不需要担心深拷贝的问题,填补了左值引用使用上的一些缺陷。
右值引用可以通过移动构造或移动赋值,对返回值是右值(将亡值)的资源进行转义,减少拷贝,延长资源生命周期。
3.3. 万能引用与完美转发
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 模板中的&&不是右值引用,而是万能引用/引用折叠,其提供了既能接收左值又能接收右值的能力
// 但在接收之后的使用中,值类型都退化成了左值
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
void Test8()
{
int i = 1;
PerfectForward(i); // 左值
PerfectForward(move(i)); // 右值
const int ci = 2;
PerfectForward(ci); // const 左值
PerfectForward(move(ci)); // const 右值
}
// 如果想要在传递过程中保持最初的左值或右值属性,就需要用到完美转发
template<typename T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
}
3.4. 移动构造与移动赋值
C++11STL容器中都是增加了移动构造和移动赋值的。
移动构造和移动赋值是C++11新增的两个默认成员函数。
当没有自己实现移动构造函数,且没有实现析构、拷贝构造、拷贝赋值中的任何一个函数。那么编译器会自动生成一个默认的移动构造。其对于内置类型成员会执行按字节拷贝(值拷贝);对于自定义类型成员,会看这个成员是否实现了移动构造,如果实现了就调用,没有实现就调用拷贝构造。移动赋值同理。
4. default & delete
default和delete的使用可以让我们更好地控制默认函数的生成。
default可以强制某个默认函数的生成。比如:提供拷贝构造后,移动构造就不生成了,那么可以使用default显示指定移动构造生成。
如果不想要某个默认函数生成,只需要在函数声明处加上=delete
即可。=delete
修饰的函数也称为删除函数。
// 要求delete关键字实现一个类,只能在堆上创建对象
class HeapOnly
{
public:
HeapOnly()
{
_str = new char[10];
}
~HeapOnly() = delete;
void Destroy()
{
delete[] _str;
operator delete(this);
}
private:
char* _str;
};
void Test9()
{
HeapOnly* ptr = new HeapOnly;
ptr->Destroy();
}
5. 可变参数模板
相比C++98固定的模板参数,C++11可变参数模板的的引入无疑是一个巨大的改进。
// 一个基本的函数可变参数模板定义
// Args是一个模板参数包,args是一个函数形参参数包,包含有大于等于0个参数
template<class ...Args>
void ShowList(Args... args)
{}
参数args前面有省略号,表示它是一个可变模板参数。把带有省略号的参数称为“参数包”,里面包含了N(N>=0)个模板参数。由于语法不支持使用args[i]这样的方式获取参数,需要使用一些特别的方法来一一获取参数包中的参数。
- 递归展开参数包
void ShowList() {} // 递归终止函数
template <class T, class ...Args>
void ShowList(const T& val, Args... args)
{
cout << "ShowList(" << val << ", " << sizeof...(args) <<"个参数)" << endl;
ShowList(args...);
}
void Test10()
{
ShowList(1, 'A', string("one"));
}
2. 构建数组展开参数包
template<class T>
int PrintArg(const T& val)
{
cout << val << " ";
return 0;
}
template <class ...Args>
void ShowList(Args... args)
{
int a[] = { PrintArg(args)... };
cout << sizeof(a) / sizeof(int) << endl;
}
void Test11()
{
ShowList(1, 'A', string("one"));
}
{ PrintArg(args)... }
将会展开成 { PrintArg(arg1), PrintArg(arg2), PrintArg(arg3), ... }
,最终会创建一个元素值都为PrintArg
返回值的数组。也就是说在构建数组的过程中参数包就展开了,而这个数组构建的目的纯粹就是为了在数组构建的过程中展开参数包。
6. push_back 与 emplace_back
push_back 与 emplace_back都是在end位置插入一个值。
可以注意到emplace接口支持可变模板参数,并且使用了万能引用。那么相对传统的插入接口,emplace接口优势到底在哪里呢?
在用法上,emplace支持可变参数,拿到参数后,会自己去创建对象,简化使用。
void Test12()
{
vector<pair<int, int>> v;
v.push_back(make_pair(1,1));
v.push_back({ 2,2 });
v.emplace_back(3, 3);
}
在底层上,emplace_back是直接构造,push_back是先构造,再拷贝构造/移动构造。
emplace_back直接构造确实会效率高一点,但实际上emplack_back和push_back的使用效果是差别不大的。
7. lambda表达式
lambda表达式实际是一个匿名函数。
lambda表达式语法:[capture-list](parameters)mutable->return-type{statement}
。
[capture-list]
:捕捉列表。捕捉列表能够捕捉上下文中的变量供lambda使用,同时编译器也会根据[]
来判断接下来的代码是否为lambda函数。(parameters)
:参数列表。和普通函数的参数列表使用方式一致。如果不需要传参,()
也可以省略。mutable
:默认情况下,lambda函数是一个const
函数,而mutable
可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数列表为空)。->returntype
:返回值类型声明。无返回值时可省略;返回值类型明确的情况下,也可省略,交由编译器自行推导返回值类型。{statement}
:函数体。在函数体内可以使用参数列表的参数和捕捉列表捕获的变量。
从lambda表达式定义中,可以知道参数列表和返回值是可选部分,捕捉列表和函数体是可为空部分。所以最简单的lambda函数是[]{}
。
7.1. 捕捉列表
- [var]:表示传值方式捕捉变量var
- [=]:表示传值方式捕捉父作用域中的所有变量(包括this)
- [&var]:表示传引用方式捕捉变量var
- [&]:表示传引用方式捕捉父作用域中的所有变量(包括this)
- [this]:表示传值方式捕捉this指针
void Test13()
{
int x = 1;
int y = 2;
// 默认捕捉(传值捕捉)的变量不能修改
// auto swap2 = [=]
auto swap2 = [=]()mutable
{
int tmp = x;
x = y;
y = tmp;
};
swap2();
cout << x << " " << y << endl;
}
即使对于传值捕捉的变量进行了修改,也不会对外部实际的变量造成修改。
所谓父作用域是指与lambda所在栈帧平行的栈帧空间。
static int si = 1;
void Test14()
{
int i = 2;
auto a = [=] {cout << si << endl; cout << i << endl; };
a();
}
捕捉列表可由多个项组成,并以逗号分割。
void Test15()
{
int a, b, c, d, e;
a = b = c = d = e = 1;
// 全部传值捕捉
auto f1 = [=]()
{
cout << a << b << c << d << e << endl;
};
f1();
cout << a << endl;
// 混合捕捉
auto f2 = [=, &a]()
{
++a;
cout << a << b << c << d << e << endl;
};
f2();
cout << a << endl;
auto f3 = [&, a]()mutable
{
++a;
cout << a << b << c << d << e << endl;
};
f3();
cout << a << endl;
}
捕捉列表不允许变量重复传递。比如[=, this],this捕捉会重复,从而导致编译错误。
lambda表达式之间不能相互赋值。
void Test16()
{
auto f1 = [] {cout << "hello world"; };
auto f2 = [] {cout << "hello world"; };
//f2 = f1; // err
cout << typeid(f1).name() << endl;
cout << typeid(f2).name() << endl;
}
lambda对象不能相互赋值,本质原因是因为类型不同,不能相互赋值。
这也说明,lambda在使用者角度是匿名的,在底层还是有名的。
实际在底层,编译器对于lambda表达式的处理,完全是按照函数对象的方式处理的。即:如果定义了一个lambda表达式,编译器会自动生成一个类(为其生成lambda_uuid的类型名),在该类中会重载operator()。
8. function包装器
C++中的function本质是一个类模板。
那为什么要有包装器呢?
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
void Test17()
{
// 函数指针
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lambda表达式对象
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
// 函数指针 | 仿函数/函数对象 | lambda(匿名函数) -> 都能像函数一样被使用
}
可以发现useF函数模板实例化了三份。但如果使用包装器就可以很好地优化上面的问题。
Ret:被调用函数的返回类型
Args…:被调用函数的形参
void Test18()
{
// 函数指针
function<double(double)> f1 = f;
cout << useF(f1, 11.11) << endl;
// 函数对象
function<double(double)> f2 = Functor();
cout << useF(f2, 11.11) << endl;
// lambda表达式对象
function<double(double)> f3 = [](double d)->double {return d / 4; };
cout << useF(f3, 11.11) << endl;
}
8.1. bind
可以把bind看做是函数适配器。bind的作用主要是可以接收一个可调用对象(callable object),同时生成一个新的可调用对象来“适应”原对象的参数列表。使用bind我们可以把一个原本接收 n 个参数的函数 f,通过绑定一些参数,然后返回一个接收 m 个参数的新函数,同时可以做到对参数顺序的调整。
bind的一般使用形式:auto newCallable = bind(callable, arg_list)
。
newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应callable的参数。当调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。
arg_list中的参数可能包含形如 _n (n 是一个整数)的名字,这些参数是“占位符”,表示newCallable的参数。这些“占位符”占据了传递给newCallable的参数的“位置”。例如,_1为newCallable的第一个参数,_2为第二个参数,…
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
void Test19()
{
function<int(int, int)> f1 = Plus::plusi;
function<double(Plus, double, double)> f2 = &Plus::plusd;
cout << f1(1, 2) << endl;
cout << f2(Plus(), 1.1, 2.2) << endl;
// 调整参数个数,绑死固定参数
function<int(int, int)> f3 = bind(Plus::plusi, placeholders::_1, placeholders::_2);
function<double(double, double)> f4 = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);
cout << f3(1, 2) << endl;
cout << f4(1.1, 2.2) << endl;
}