佛山建设网站,杭州模板网站建站,国家工商注册网官网,网站建设与管理课程总结1.右值引用右值引用是 C11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C 中大量的历史遗留问题#xff0c; 消除了诸如 std::vector、std::string 之类的额外开销#xff0c; 也才使得函数对象容器 std::function 成为了可能。1.1左值、右值的纯右值、将亡值…1.右值引用右值引用是 C11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C 中大量的历史遗留问题 消除了诸如 std::vector、std::string 之类的额外开销 也才使得函数对象容器 std::function 成为了可能。1.1左值、右值的纯右值、将亡值、右值要弄明白右值引用到底是怎么一回事必须要对左值和右值做一个明确的理解。左值 (lvalue, left value)顾名思义就是赋值符号左边的值。准确来说 左值是表达式不一定是赋值表达式后依然存在的持久对象。右值 (rvalue, right value)右边的值是指表达式结束后就不再存在的临时对象。而 C11 中为了引入强大的右值引用将右值的概念进行了进一步的划分分为纯右值、将亡值。纯右值 (prvalue, pure rvalue)纯粹的右值要么是纯粹的字面量例如 10, true 要么是求值结果相当于字面量或匿名临时对象例如 12。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。需要注意的是字面量除了字符串字面量以外均为纯右值。而字符串字面量是一个左值类型为 const char 数组。例如#include type_traits
int main() {// 正确01234 类型为 const char [6]因此是左值const char (left)[6] 01234;// 断言正确确实是 const char [6] 类型注意 decltype(expr) 在 expr 是左值// 且非无括号包裹的 id 表达式与类成员表达式时会返回左值引用static_assert(std::is_samedecltype(01234), const char()[6]::value, );// 错误01234 是左值不可被右值引用// const char (right)[6] 01234;
}但是注意数组可以被隐式转换成相对应的指针类型而转换表达式的结果如果不是左值引用则一定是个右值右值引用为将亡值否则为纯右值。例如const char* p 01234; // 正确01234 被隐式转换为 const char*
const char* pr 01234; // 正确01234 被隐式转换为 const char*该转换的结果是纯右值
// const char* pl 01234; // 错误此处不存在 const char* 类型的左值
将亡值 (xvalue, expiring value)是 C11 为了引入右值引用而提出的概念因此在传统 C 中 纯右值和右值是同一个概念也就是即将被销毁、却能够被移动的值。
将亡值可能稍有些难以理解我们来看这样的代码
std::vectorint foo() {std::vectorint temp {1, 2, 3, 4};return temp;
}
std::vectorint v foo();在这样的代码中就传统的理解而言函数 foo 的返回值 temp 在内部创建然后被赋值给 v 然而 v 获得这个对象时会将整个 temp 拷贝一份然后把 temp 销毁如果这个 temp 非常大 这将造成大量额外的开销这也就是传统 C 一直被诟病的问题。在最后一行中v 是左值、 foo() 返回的值就是右值也是纯右值。但是v 可以被别的变量捕获到 而 foo() 产生的那个返回值作为一个临时值一旦被 v 复制后将立即被销毁无法获取、也不能修改。 而将亡值就定义了这样一种行为临时的值能够被识别、同时又能够被移动。在 C11 之后编译器为我们做了一些工作此处的左值 temp 会被进行此隐式右值转换 等价于 static_caststd::vectorint (temp)进而此处的 v 会将 foo 局部返回的值进行移动。 也就是后面我们将会提到的移动语义。1.2右值引用和左值引用要拿到一个将亡值就需要用到右值引用T 其中 T 是类型。 右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着那么将亡值将继续存活。C11 提供了 std::move 这个方法将左值参数无条件的转换为右值 有了它我们就能够方便的获得一个右值临时对象例如#include iostream
#include string
void reference(std::string str) {std::cout 左值 std::endl;
}
void reference(std::string str) {std::cout 右值 std::endl;
}
int main()
{std::string lv1 string,; // lv1 是一个左值// std::string r1 lv1; // 非法, 右值引用不能引用左值std::string rv1 std::move(lv1); // 合法, std::move可以将左值转移为右值std::cout rv1 std::endl; // string,const std::string lv2 lv1 lv1; // 合法, 常量左值引用能够延长临时变量的生命周期// lv2 Test; // 非法, 常量引用无法被修改std::cout lv2 std::endl; // string,string,std::string rv2 lv1 lv2; // 合法, 右值引用延长临时对象生命周期rv2 Test; // 合法, 非常量引用能够修改临时变量std::cout rv2 std::endl; // string,string,string,Testreference(rv2); // 输出左值return 0;
}rv2 虽然引用了一个右值但由于它是一个引用所以 rv2 依然是一个左值。注意这里有一个很有趣的历史遗留问题我们先看下面的代码#include iostream
int main() {// int a std::move(1); // 不合法非常量左引用无法引用右值const int b std::move(1); // 合法, 常量左引用允许引用右值std::cout a b std::endl;
}
第一个问题为什么不允许非常量引用绑定到非左值这是因为这种做法存在逻辑错误
void increase(int v) {v;
}
void foo() {double s 1;increase(s);
}由于 int 不能引用 double 类型的参数因此必须产生一个临时值来保存 s 的值 从而当 increase() 修改这个临时值时调用完成后 s 本身并没有被修改。第二个问题为什么常量引用允许绑定到非左值原因很简单因为 Fortran 需要。2.移动构造函数传统 C 通过拷贝构造函数和赋值操作符为类对象设计了拷贝/复制的概念但为了实现对资源的移动操作 调用者必须使用先复制、再析构的方式否则就需要自己实现移动对象的接口。 试想搬家的时候是把家里的东西直接搬到新家去而不是将所有东西复制一份重买再放到新家、 再把原来的东西全部扔掉销毁这是非常反人类的一件事情。传统的 C 没有区分『移动』和『拷贝』的概念造成了大量的数据拷贝浪费时间和空间。 右值引用的出现恰好就解决了这两个概念的混淆问题例如#include iostream
class A {
public:int *pointer;A():pointer(new int(1)) {std::cout 构造 pointer std::endl;}A(A a):pointer(new int(*a.pointer)) {std::cout 拷贝 pointer std::endl;} // 无意义的对象拷贝A(A a):pointer(a.pointer) {a.pointer nullptr;std::cout 移动 pointer std::endl;}~A(){std::cout 析构 pointer std::endl;delete pointer;}
};
// 防止编译器优化
A return_rvalue(bool test) {A a,b;if(test) return a; // 等价于 static_castA(a);else return b; // 等价于 static_castA(b);
}
int main() {A obj return_rvalue(false);std::cout obj: std::endl;std::cout obj.pointer std::endl;std::cout *obj.pointer std::endl;return 0;
}在上面的代码中首先会在 return_rvalue 内部构造两个 A 对象于是获得两个构造函数的输出函数返回后产生一个将亡值被 A 的移动构造A(A)引用从而延长生命周期并将这个右值中的指针拿到保存到了 obj 中而将亡值的指针被设置为 nullptr防止了这块内存区域被销毁。从而避免了无意义的拷贝构造加强了性能。再来看看涉及标准库的例子#include iostream // std::cout
#include utility // std::move
#include vector // std::vector
#include string // std::string
int main() {std::string str Hello world.;std::vectorstd::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;return 0;
}2.1完美的移动转发前面我们提到了一个声明的右值引用其实是一个左值。这就为我们进行参数转发传递造成了问题void reference(int v) {std::cout 左值 std::endl;
}
void reference(int v) {std::cout 右值 std::endl;
}
template typename T
void pass(T v) {std::cout 普通传参:;reference(v); // 始终调用 reference(int)
}
int main() {std::cout 传递右值: std::endl;pass(1); // 1是右值, 但输出是左值std::cout 传递左值: std::endl;int l 1;pass(l); // l 是左值, 输出左值return 0;
}对于 pass(1) 来说虽然传递的是右值但由于 v 是一个引用所以同时也是左值。 因此 reference(v) 会调用 reference(int)输出『左值』。 而对于pass(l)而言l是一个左值为什么会成功传递给 pass(T) 呢这是基于引用坍缩规则的在传统 C 中我们不能够对一个引用类型继续进行引用 但 C 由于右值引用的出现而放宽了这一做法从而产生了引用坍缩规则允许我们对引用进行引用 既能左引用又能右引用。但是却遵循如下规则函数形参类型实参参数类型推导后函数形参类型T左引用TT右引用TT左引用TT右引用T因此模板函数中使用 T 不一定能进行右值引用当传入左值时此函数的引用将被推导为左值。 更准确的讲无论模板参数是什么类型的引用当且仅当实参类型为右引用时模板参数才能被推导为右引用类型。 这才使得 v 作为左值的成功传递。完美转发就是基于上述规律产生的。所谓完美转发就是为了让我们在传递参数的时候 保持原来的参数类型左引用保持左引用右引用保持右引用。 为了解决这个问题我们应该使用 std::forward 来进行参数的转发传递#include iostream
#include utility
void reference(int v) {std::cout 左值引用 std::endl;
}
void reference(int v) {std::cout 右值引用 std::endl;
}
template typename T
void pass(T v) {std::cout 普通传参: ;reference(v);std::cout std::move 传参: ;reference(std::move(v));std::cout std::forward 传参: ;reference(std::forwardT(v));std::cout static_castT 传参: ;reference(static_castT(v));
}
int main() {std::cout 传递右值: std::endl;pass(1);std::cout 传递左值: std::endl;int v 1;pass(v);return 0;
}输出结果为传递右值: 普通传参: 左值引用std::move 传参: 右值引用std::forward 传参: 右值引用
static_castT 传参: 右值引用
传递左值:普通传参: 左值引用std::move 传参: 右值引用std::forward 传参: 左值引用
static_castT 传参: 左值引用无论传递参数为左值还是右值普通传参都会将参数作为左值进行转发 所以 std::move 总会接受到一个左值从而转发调用了reference(int) 输出右值引用。唯独 std::forward 即没有造成任何多余的拷贝同时完美转发(传递)了函数的实参给了内部调用的其他函数。std::forward 和 std::move 一样没有做任何事情std::move 单纯的将左值转化为右值 std::forward 也只是单纯的将参数做了一个类型的转换从现象上来看 std::forwardT(v) 和 static_castT(v) 是完全一样的。读者可能会好奇为何一条语句能够针对两种类型的返回对应的值 我们再简单看一看 std::forward 的具体实现机制std::forward 包含两个重载templatetypename _Tp
constexpr _Tp forward(typename std::remove_reference_Tp::type __t) noexcept
{ return static_cast_Tp(__t); }
templatetypename _Tp
constexpr _Tp forward(typename std::remove_reference_Tp::type __t) noexcept
{static_assert(!std::is_lvalue_reference_Tp::value, template argument substituting _Tp is an lvalue reference type);return static_cast_Tp(__t);
}在这份实现中std::remove_reference 的功能是消除类型中的引用 std::is_lvalue_reference 则用于检查类型推导是否正确在 std::forward 的第二个实现中 检查了接收到的值确实是一个左值进而体现了坍缩规则。当 std::forward 接受左值时_Tp 被推导为左值所以返回值为左值而当其接受右值时 _Tp 被推导为 右值引用则基于坍缩规则返回值便成为了 的右值。 可见 std::forward 的原理在于巧妙的利用了模板类型推导中产生的差异。这时我们能回答这样一个问题为什么在使用循环语句的过程中auto 是最安全的方式 因为当 auto 被推导为不同的左右引用时与 的坍缩组合是完美转发。