[toc]
1. nullptr
nullptr出现的目的是为了替代NULL。
在某种意义上,传统C++会把NULL、0视为同一种东西,这取决于编译器如**何定义NULL,有些编译器会将NULL定义为((void*)0),有些则会直接将其定义为0.
C++不允许直接将void* 隐式转换到其他类型,但如果NULL被定义为((void*)0),那么当编译char *ch = NULL,时,NULL只好被定义为0。而这仍然会产生问题,将导致C++中重载特性会发生混乱。
void foo(char *);
void foo(int)
对这两个函数来说,NULL又被定义为了0,那么foo(NULL);这个语句将会调用foo(int),违反直观。
为了解决这个问题,C++11引入了nullptr关键字,专门用来区分空指针、0.
nullptr的类型为nullptr_t,能够隐式的转换为任何指针或者成员指针的类型,也能和他们进行相等或者不等的比较。
2. 类型推导
c++11 引入了auto 和 decltype这两个关键字实现了类型推导,让编译器来操心变量的类型。
auto
auto在很早之前就已经引入C++。。
使用auto之前的迭代器写法
for(vector<int>::const_iterator itr = vec.cbegin();itr != vec.cend();++itr)
之后
for(auto itr = vec.cbegin();itr!=vec.cend();++itr);
一些其他用法
auto i = 5;
auto arr = new auto(10);
- 注意auto不能函数传参。 考虑重载的话应该用模板
- 此外auto还不能推导数组类型
decltype
decltype的出现是为了解决auto关键字只能对变量进行类型推导的缺陷而出现的。它的用法和sizeof很相似:
decltype(表达式)
再次过程中,编译器分析表达式得到他的类型,却不实际计算表达式的值。
有时候,我们可能需要计算某个表达式的类型,例如:
auto x = 1;
auto y = 2;
decltype(x + y) z
拖尾返回类型、auto与decltype配合
考虑到一个加法函数的例子,在传统C++中,我们需要
template<typename R,typename T,typename U>
R add(T x,U y){
return x + y;
}
这样的代码很丑陋,程序员在使用这个模板函数的时候,必须指出返回类型,但事实上并不知道add这个函数会做什么样的操作,获得一个什么样的类型。
C++11 引入了拖尾返回类型 trailing return type ,利用auto关键字将返回类型后置:
template<typename T,typename U>
auto add(T x, U y) ->decltype(x+y){
return x + y;
}
C++14 开始,可以直接让普通函数具备返回值推导,因此下面的写法合法。
template<typename T, typename U>
auto add(T x, U y){
return x + y;
}
3.区间迭代
基于范围的for循环
C++11引入了基于范围的迭代写法,我们拥有了能够写出像python一样简介的循环语句。
最常用的std::vector遍历将从原来的样子:
std::vector<int>arr(5,100);
for(std::vector<int>::iterator i = arr.begin(); i != arr.end(); ++i){
std::cout << *i << std::endl;
}
变得非常简单
for(auto &i : arr){
std::cout << i << std::endl;
}
4. 初始化列表
C++11提供了统一的语法来初始化任意的对象
struct A{
int a;
float b;
};
struct B{
B(int _a,float _b):a(_a),b(_b){}
private:
int a;
float b;
};
A a {1,1.1};
B b {2,2.2};
C++11还把初始化列表的概念绑定到类型上,并将其称之为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数据和POD的初始化方法提供了统一的桥梁。
#include<initializer_list>
class Magic{
public:
Magic(std::initializer_list<int>list){}
};
Magic magic = {1,2,3,4,5};
std::vector<int>v = {1,2,3,4};
5.模板增强
外部模板
传统C++中,模板只有在使用时才会被编译器实例化。 只要在每个编译单元(文件)中编译的代码遇到了被完整定义的模板,都会实例化。 这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要出发模板实例化
C++ 11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使得能够显式的告诉编译器何时进行模板的实例化
template class std::vector<bool>; // 强行实例化
extern template class std::vector<double>; // 不在该编译文件中实例化模板
尖括号 “>”
传统C++ >> 不能连续写,会被当做右值符。 C++11可以
类型别名模板
- 在传统C++中typedef 不能为模板定义一个新的名词, 因为模板不是类型。
- C++11 使用using引入了下面这种形式的写法,同时支持typedef 相同的功效。
template<typename T>
using NewType = SuckType<int,T,1>; // 合法
默认模板参数
template<typename T, typename U>
auto add(T x, U y) -> decltype(x + y){
return x + y;
}
但在使用中需要显示指定 模板参数的类型。
在C++11中提供了一种便利,可以指定模板的默认参数:
template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x + y){
return x + y;
}
6.构造函数
委托构造
C++11引入了委托构造的概念,同一个类中,构造函数调用另一个构造函数
class Base{
public:
int value1;
int value2;
Base(){
value1 = 1;
}
Base(int value) : Base(){
value2 = 2;
}
};
继承构造
struct A{
A(int i){}
A(double d, int i){}
A(float f,int i,const char* c){}
};
struct B:A{
B(int i):A(i){}
B(double d, int i):A(d,i){}
B(float f, itn i,const char* c):A(f,i,e){}
};
C++11的继承构造:
struct B:A{
using A::A;
// 关于基类各构造函数的继承一句话搞定
}
且如果一个继承构造函数不被相关代码使用,编译器不会为之产生真正的函数代码,这样比透传基类各种构造函数更加节省目标代码空间。
7.Lambda表达式
类似于匿名函数。
基本语法如下:
[capture] (params) opt -> ret{body;};
- capture是捕获列表
- params是参数列表(选填)
- opt是函数选项;可以填 mutable,exception,attribute(选填)
mutable说明 lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的not-const方法。
exception说明lambda表达式是否抛出异常以及何种异常。
attribute用来声明属性。 - ret是返回值类型(拖尾返回类型)。(选填)
- body是函数体。
捕获列表 lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。
1. [] 不捕获任何变量
2. [&] 捕获外部作用域中所有变量,并作为引用在函数体内使用(按引用捕获)
3. [=] 捕获外部作用于中所有变量,并作为副本在函数体内使用(按值捕获)。 注意值捕获的前提是变量可以拷贝,且被捕获的变量在lambda表达式被创建时拷贝,而非调用时才拷贝。 如果希望lambda表达式在调用时,能即时访问外部变量,我们应用使用引用方式。
int a = 0;
auto f = [=] {return a;};
a+= 1;
cout << f() <<endl; //输出0
int a = 0;
auto f = [&a] {return a;};
a+= 1;
cout<< f()<<endl;
- [=,&foo] 按值捕获外部作用域中所有变量,并按照引用捕获foo变量。
- [bar]按照值捕获bar变量,同时不捕获其他变量。
- [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了& 或者 = ,就默认添加此选项。 捕获this的目的是可以在lambda中使用当前类的成员函数和成员变量
class A
{
public:
int i_ = 0;
void func(int x,int y){
auto x1 = [] { return i_; }; //error,没有捕获外部变量
auto x2 = [=] { return i_ + x + y; }; //OK
auto x3 = [&] { return i_ + x + y; }; //OK
auto x4 = [this] { return i_; }; //OK
auto x5 = [this] { return i_ + x + y; }; //error,没有捕获x,y
auto x6 = [this, x, y] { return i_ + x + y; }; //OK
auto x7 = [this] { return i_++; }; //OK
};
int a=0 , b=1;
auto f1 = [] { return a; }; //error,没有捕获外部变量
auto f2 = [&] { return a++ }; //OK
auto f3 = [=] { return a; }; //OK
auto f4 = [=] {return a++; }; //error,a是以复制方式捕获的,无法修改
auto f5 = [a] { return a+b; }; //error,没有捕获变量b
auto f6 = [a, &b] { return a + (b++); }; //OK
auto f7 = [=, &b] { return a + (b++); }; //OK
注意f4,虽然按值捕获的变量值均复制一份存储在lambda表达式变量中,修改他们也并不会真正影响到外部,但我们却仍然无法修改它们。如果希望去修改按值捕获的外部变量,需要显示指明lambda表达式为mutable。被mutable修饰的lambda表达式就算没有参数也要写明参数列表。
原因:lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终会变为闭包类型的成员变量。按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量的值的。而mutable的作用,就在于取消operator()的const。
int a = 0;
auto f1 = [=] { return a++; }; //error
auto f2 = [=] () mutable { return a++; }; //OK
lambda表达式的大致原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,是一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,与具体实现有关。
lambda表达式是不能被赋值的:
auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };
a = b; // 非法,lambda无法赋值
auto c = a; // 合法,生成一个副本
闭包类型禁用了赋值操作符,但是没有禁用复制构造函数,所以你仍然可以用一个lambda表达式去初始化另外一个lambda表达式而产生副本。
在多种捕获方式中,最好不要使用[=]和[&]默认捕获所有变量。
默认引用捕获所有变量,你有很大可能会出现悬挂引用(Dangling references),因为引用捕获不会延长引用的变量的生命周期:
std::function<int(int)> add_x(int x){
return [&](int a){return x + a;};
}
返回的lambda 中的x是垃圾值。
上面函数返回了一个lambda表达式,参数x仅是一个临时变量,函数add_x调用后就被销毁了,但是返回的lambda表达式却引用了该变量,当调用这个表达式时,引用的是一个垃圾值,会产生没有意义的结果。上面这种情况,使用默认传值方式可以避免悬挂引用问题。
但是采用默认值捕获所有变量仍然有风险,看下面的例子:
class Filter
{
public:
Filter(int divisorVal):
divisor{divisorVal}
{}
std::function<bool(int)> getFilter()
{
return [=](int value) {return value % divisor == 0; };
}
private:
int divisor;
};
这个类中有一个成员方法,可以返回一个lambda表达式,这个表达式使用了类的数据成员divisor。而且采用默认值方式捕捉所有变量。你可能认为这个lambda表达式也捕捉了divisor的一份副本,但是实际上并没有。因为数据成员divisor对lambda表达式并不可见,你可以用下面的代码验证:
// 类的方法,下面无法编译,因为divisor并不在lambda捕捉的范围
std::function<bool(int)> getFilter()
{
return [divisor](int value) {return value % divisor == 0; };
}
原代码中,lambda表达式实际上捕捉的是this指针的副本,所以原来的代码等价于:
std::function<bool(int)> getFilter()
{
return [this](int value) {return value % this->divisor == 0; };
}
尽管还是以值方式捕获,但是捕获的是指针,其实相当于以引用的方式捕获了当前类对象,所以lambda表达式的闭包与一个类对象绑定在一起了,这很危险,因为你仍然有可能在类对象析构后使用这个lambda表达式,那么类似“悬挂引用”的问题也会产生。所以,采用默认值捕捉所有变量仍然是不安全的,主要是由于指针变量的复制,实际上还是按引用传值。
lambda表达式可以赋值给对应类型的函数指针。但是使用函数指针并不是那么方便。所以STL定义在< functional >头文件提供了一个多态的函数对象封装std::function,其类似于函数指针。它可以绑定任何类函数对象,只要参数与返回类型相同。如下面的返回一个bool且接收两个int的函数包装器:
std::function<bool(int ,int )> wrapper = [](int x,int y){return x <y;};
lambda表达式一个更重要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数
最常用的是STL算法汇总,比如要统计一个数组中满足特定条件的元素数量。 通过lambda表达式给出条件,传递给count_if.
int value = 3;
vector<int> v {1,3,5,3,3,2,6,10};
int count = std::count_if(v.cbegin(),v.cend(),[value](int x){return x > value;});
再比如你想生成斐波那契数列,然后保存在数组中,此时你可以使用generate函数,并辅助lambda表达式:
vector<int> v(10);
int a = 0;
int b = 1;
std::generate(v.begin(), v.end(), [&a, &b] { int value = b; b = b + a; a = value; return value; });
// 此时v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}
当需要遍历容器并对每个元素进行操作时:
std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
int even_count = 0;
for_each(v.begin(), v.end(), [&even_count](int val){
if(!(val & 1)){
++ even_count;
}
});
std::cout << "The number of even is " << even_count << std::endl;
大部分STL算法,可以非常灵活地搭配lambda表达式来实现想要的效果。
8. 新增容器
std::array
std::array 保存在栈空间中,相比堆内存中的std::vector,我们能够灵活的访问这里面的元素,从而获得更高的性能。
std::array 会在编译时创建一个固定大小的数组,std::array不能被隐式的转换成指针,使用std::array只需指定其类型和大小即可。
std::array<int,4> arr = {1,2,3,4};
int len = 4;
std::array<int,len> arr = {1,2,3,4}; // 非法,数组大小参数必须是const
当使用 std::array() 难免要兼容C风格接口
void foo(int *p,int len){
cout<<len<<endl;
return;
}
std::array<int,4> arr = {1,2,3,4};
foo(&arr[0], arr.size());
foo(arr.data(), arr.size())
std::forward_list
std::forward_list 是一个列表容器,使用方法和 std::list 基本类似。
和 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问(这也是链表的特点),也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向迭代时,具有比 std::list 更高的空间利用率。
无序容器
C++11 引入了两组无序容器:
std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。
无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(constant)。
元组
元组的使用有三个核心的函数:
- std::make_tuple: 构造元组
- std::get: 获得元组某个位置的值
- std::tie: 元组拆包
#include <tuple>
#include <iostream>
auto get_student(int id)
{
// 返回类型被推断为 std::tuple<double, char, std::string>
if (id == 0)
return std::make_tuple(3.8, 'A', "张三");
if (id == 1)
return std::make_tuple(2.9, 'C', "李四");
if (id == 2)
return std::make_tuple(1.7, 'D', "王五");
return std::make_tuple(0.0, 'D', "null");
// 如果只写 0 会出现推断错误, 编译失败
}
int main()
{
auto student = get_student(0);
std::cout << "ID: 0, "
<< "GPA: " << std::get<0>(student) << ", "
<< "成绩: " << std::get<1>(student) << ", "
<< "姓名: " << std::get<2>(student) << '\n';
double gpa;
char grade;
std::string name;
// 元组进行拆包
std::tie(gpa, grade, name) = get_student(1);
std::cout << "ID: 1, "
<< "GPA: " << gpa << ", "
<< "成绩: " << grade << ", "
<< "姓名: " << name << '\n';
}
合并两个元组 std::tuple_cat
auto new_tuple = std::tuple_cat(get_student(1), std::move(t));
9.正则表达式
正则表达式描述了一种字符串匹配的模式。一般使用正则表达式主要是实现下面三个需求:
1) 检查一个串是否包含某种形式的子串;
2) 将匹配的子串替换;
3) 从某个串中取出符合条件的子串。
C++11 提供的正则表达式库操作 std::string 对象,对模式 std::regex (本质是 std::basic_regex)进行初始化,通过 std::regex_match 进行匹配,从而产生 std::smatch (本质是 std::match_results 对象)。
我们通过一个简单的例子来简单介绍这个库的使用。考虑下面的正则表达式:
[a-z]+.txt: 在这个正则表达式中, [a-z] 表示匹配一个小写字母, + 可以使前面的表达式匹配多次,因此 [a-z]+ 能够匹配一个及以上小写字母组成的字符串。在正则表达式中一个 . 表示匹配任意字符,而 . 转义后则表示匹配字符 . ,最后的 txt 表示严格匹配 txt 这三个字母。因此这个正则表达式的所要匹配的内容就是文件名为纯小写字母的文本文件。
std::regex_match 用于匹配字符串和正则表达式,有很多不同的重载形式。最简单的一个形式就是传入std::string 以及一个 std::regex 进行匹配,当匹配成功时,会返回 true,否则返回 false。例如:
#include <iostream>
#include <string>
#include <regex>
int main() {
std::string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};
// 在 C++ 中 `\` 会被作为字符串内的转义符,为使 `\.` 作为正则表达式传递进去生效,需要对 `\` 进行二次转义,从而有 `\\.`
std::regex txt_regex("[a-z]+\\.txt");
for (const auto &fname: fnames)
std::cout << fname << ": " << std::regex_match(fname, txt_regex) << std::endl;
}
另一种常用的形式就是依次传入 std::string/std::smatch/std::regex 三个参数,其中 std::smatch 的本质其实是 std::match_results,在标准库中, std::smatch 被定义为了 std::match_results,也就是一个子串迭代器类型的 match_results。使用 std::smatch 可以方便的对匹配的结果进行获取,例如:
std::regex base_regex("([a-z]+)\\.txt");
std::smatch base_match;
for(const auto &fname: fnames) {
if (std::regex_match(fname, base_match, base_regex)) {
// sub_match 的第一个元素匹配整个字符串
// sub_match 的第二个元素匹配了第一个括号表达式
if (base_match.size() == 2) {
std::string base = base_match[1].str();
std::cout << "sub-match[0]: " << base_match[0].str() << std::endl;
std::cout << fname << " sub-match[1]: " << base << std::endl;
}
}
}
以上两个代码段的输出结果为:
foo.txt: 1
bar.txt: 1
test: 0
a0.txt: 0
AAA.txt: 0
sub-match[0]: foo.txt
foo.txt sub-match[1]: foo
sub-match[0]: bar.txt
bar.txt sub-match[1]: bar
10.语言级线程支持
std::thread
std::mutex/std::unique_lock
std::future/std::packaged_task
std::condition_variable
代码编译需要使用 -pthread 选项
11. 右值引用和move语义
先看一个简单的例子直观感受下:
string a(x); // line 1
string b(x + y); // line 2
string c(some_function_returning_a_string()); // line 3
如果使用以下拷贝构造函数:
string(const string& that)
{
size_t size = strlen(that.data) + 1;
data = new char[size];
memcpy(data, that.data, size);
}
以上3行中,只有第一行(line 1)的x深度拷贝是有必要的,因为我们可能会在后边用到x,x是一个左值(lvalues)。
第二行和第三行的参数则是右值,因为表达式产生的string对象是匿名对象,之后没有办法再使用了。
C++ 11引入了一种新的机制叫做“右值引用”,以便我们通过重载直接使用右值参数。我们所要做的就是写一个以右值引用为参数的构造函数:
string(string&& that) // string&& is an rvalue reference to a string
{
data = that.data;
that.data = 0;
}
我们没有深度拷贝堆内存中的数据,而是仅仅复制了指针,并把源对象的指针置空。事实上,我们“偷取”了属于源对象的内存数据。由于源对象是一个右值,不会再被使用,因此客户并不会觉察到源对象被改变了。在这里,我们并没有真正的复制,所以我们把这个构造函数叫做“转移构造函数”(move constructor),他的工作就是把资源从一个对象转移到另一个对象,而不是复制他们。
有了右值引用,再来看看赋值操作符:
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
注意到我们是直接对参数that传值,所以that会像其他任何对象一样被初始化,那么确切的说,that是怎样被初始化的呢?对于C++ 98,答案是复制构造函数,但是对于C++ 11,编译器会依据参数是左值还是右值在复制构造函数和转移构造函数间进行选择。
如果是a=b,这样就会调用复制构造函数来初始化that(因为b是左值),赋值操作符会与新创建的对象交换数据,深度拷贝。这就是copy and swap 惯用法的定义:构造一个副本,与副本交换数据,并让副本在作用域内自动销毁。这里也一样。
如果是a = x + y,这样就会调用转移构造函数来初始化that(因为x+y是右值),所以这里没有深度拷贝,只有高效的数据转移。相对于参数,that依然是一个独立的对象,但是他的构造函数是无用的(trivial),因此堆中的数据没有必要复制,而仅仅是转移。没有必要复制他,因为x+y是右值,再次,从右值指向的对象中转移是没有问题的。
总结一下:复制构造函数执行的是深度拷贝,因为源对象本身必须不能被改变。而转移构造函数却可以复制指针,把源对象的指针置空,这种形式下,这是安全的,因为用户不可能再使用这个对象了。
下面我们进一步讨论右值引用和move语义。
C++98标准库中提供了一种唯一拥有性的智能指针std::auto_ptr,该类型在C++11中已被废弃,因为其“复制”行为是危险的。
auto_ptr<Shape> a(new Triangle);
auto_ptr<Shape> b(a);
注意b是怎样使用a进行初始化的,它不复制triangle,而是把triangle的所有权从a传递给了b,也可以说成“a 被转移进了b”或者“triangle被从a转移到了b”。
auto_ptr 的复制构造函数可能看起来像这样(简化):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
auto_ptr 的危险之处在于看上去应该是复制,但实际上确是转移。调用被转移过的auto_ptr 的成员函数将会导致不可预知的后果。所以你必须非常谨慎的使用auto_ptr ,如果他被转移过。
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
显然,在持有auto_ptr 对象的a表达式和持有调用函数返回的auto_ptr值类型的make_triangle()表达式之间一定有一些潜在的区别,每调用一次后者就会创建一个新的auto_ptr对象。这里a 其实就是一个左值(lvalue)的例子,而make_triangle()就是右值(rvalue)的例子。
转移像a这样的左值是非常危险的,因为我们可能调用a的成员函数,这会导致不可预知的行为。另一方面,转移像make_triangle()这样的右值却是非常安全的,因为复制构造函数之后,我们不能再使用这个临时对象了,因为这个转移后的临时对象会在下一行之前销毁掉。
我们现在知道转移左值是十分危险的,但是转移右值却是很安全的。如果C++能从语言级别支持区分左值和右值参数,我就可以完全杜绝对左值转移,或者把转移左值在调用的时候暴露出来,以使我们不会不经意的转移左值。
C++ 11对这个问题的答案是右值引用。右值引用是针对右值的新的引用类型,语法是X&&。以前的老的引用类型X& 现在被称作左值引用。
使用右值引用X&&作为参数的最有用的函数之一就是转移构造函数X::X(X&& source),它的主要作用是把源对象的本地资源转移给当前对象。
C++ 11中,std::auto_ptr< T >已经被std::unique_ptr< T >所取代,后者就是利用的右值引用。
其转移构造函数:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
`
``
这个转移构造函数跟auto_ptr中复制构造函数做的事情一样,但是它却只能接受右值作为参数。
```cpp
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
第二行不能编译通过,因为a是左值,但是参数unique_ptr&& source只能接受右值,这正是我们所需要的,杜绝危险的隐式转移。第三行编译没有问题,因为make_triangle()是右值,转移构造函数会将临时对象的所有权转移给对象c,这正是我们需要的。
转移左值
有时候,我们可能想转移左值,也就是说,有时候我们想让编译器把左值当作右值对待,以便能使用转移构造函数,即便这有点不安全。出于这个目的,C++ 11在标准库的头文件< utility >中提供了一个模板函数std::move。实际上,std::move仅仅是简单地将左值转换为右值,它本身并没有转移任何东西。它仅仅是让对象可以转移。
以下是如何正确的转移左值:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
请注意,第三行之后,a不再拥有Triangle对象。不过这没有关系,因为通过明确的写出std::move(a),我们很清楚我们的意图:亲爱的转移构造函数,你可以对a做任何想要做的事情来初始化c;我不再需要a了,对于a,您请自便。
当然,如果你在使用了mova(a)之后,还继续使用a,那无疑是搬起石头砸自己的脚,还是会导致严重的运行错误。
总之,std::move(some_lvalue)将左值转换为右值(可以理解为一种类型转换),使接下来的转移成为可能。
一个例子:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
上面的parameter,其类型是一个右值引用,只能说明parameter是指向右值的引用,而parameter本身是个左值。(Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.)
因此以上对parameter的转移是不允许的,需要使用std::move来显示转换成右值。(待探讨)
override
针对派生类,名字与虚函数相同,但形参不同,而我们希望重写虚函数,所以加上override,让编译器帮我们检查这种情况,出现则报错。
https://www.cnblogs.com/xinxue/p/5471708.html
noexcept
告诉编译器这个函数不会出异常,优化速度。
推荐写出noexcept,
bd 假定小内存分配一定成功、大内存分配需要检查错误、代码库标准库抛出的异常直接让程序去死。
能用const 都要用const
noexcept用在4个地方
1. move构造函数
2. move拷贝函数
3. swap
4. 内存分配器的deallocate
写一个完美转发的例子。
std::move
右值 直接swap 不用copy。
vector emplace_back 深入调研一下. 只进行构造函数。 也没有拷贝构造 也没有移动构造
std::move 和 std::swap 有什么区别?
move是staticcast,只是让编译器认为 value_catogory发生了编号,即右值。
value_catogory > lvalue rvalue (xvalue) 这个概念只存在于编译期,本质是静态派发。
swap交换了。
函数内部,本身已经是右值引用了,为什么传递进去嗨哟啊再次move一次? 因为进去以后会被当做左值来用,所以。
完美转发的应用场景。
写数据库,float int xx。
## 参考
https://blog.csdn.net/jiange_zh/article/details/79356417