判别:
int x = 10;
int* p = &++x; // 正确,前置++返回左值
int* q = &x++; // 错误,后置++返回临时对象
左值引用就是普通的引用,下面介绍右值引用:
#include
using namespace std;
int main() {
int x = 1;
int&& r = x++;
cout << r << endl; // 1
cout << x << endl; // 2
x = 10;
cout << r << endl; // 1
return 0;
}
引用了临时的右值对象,相当于延长了生命周期。
转移语义,使用的std::move()
进行转移对象,把一个对象(左值)转移成匿名的右值。它的意义在于减少不必要的拷贝操作,提高程序性能。本质上,std::move
是使得对象的所有权发生了转移,给出代码实例:
#include
#include
#include
int main() {
std::string str = "Hello";
std::vector<std::string> v;
// 这里是对str的一个拷贝,拷贝了字符串的副本到vector中
v.push_back(str);
std::cout << "After copy, str is \"" << str << "\"\n";
// 这里是直接把str内容转移到vector中,没发生任何拷贝
// 原来的左值被转移后,str又成为了空串
v.push_back(std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are \"" << v[0] \
<< "\", \"" << v[1] << "\"\n";
return 0;
}
/*
代码输出:
After copy, str is "Hello"
After move, str is ""
The contents of the vector are "Hello", "Hello"
*/
上述代码解决了一个效率问题,每次不用拷贝一遍字符串到vector
中,而是直接使用原来的字符串。注意到,容器的本身不能存储元素的引用,这在处理复杂对象的时候,会进行大量的拷贝,浪费时间和内存;但是,借助于std::move
可以直接把左值对象的值进行转移,从而省去大量的复制步骤!
对于自定义的对象,需要显示的说明转移构造函数,代码实例:
#include
#include
#include
class Node {
public:
// 默认无参数构造函数
Node() {
std::cout << "default construction" << std::endl;
a = 0;
str = "";
p = nullptr;
}
// 普通的拷贝函数
Node(const Node& node) {
std::cout << "copy construction" << std::endl;
a = node.a;
str = node.str;
p = node.p;
}
// 移动构造函数,注意不能声明为const !!! 为确保安全,声明为不抛出异常的,
// 移动失败后直接退出程序,否则有悬空指针是非常危险的。
Node(Node&& node) noexcept {
std::cout << "move construction" << std::endl;
a = std::move(node.a);
str = std::move(node.str); // 注意使用move提高效率,string类型是可以直接move的
p = node.p;
node.p = nullptr; // 置空原来的指针
}
// 析构函数
~Node() {
std::cout << "deconstruction" << std::endl;
a = 0;
str.clear();
if(!p) {
delete p;
p = nullptr;
}
}
int a;
int* p;
std::string str;
};
int main() {
Node n; // 正常的默认构造函数
n.str = "hello world !";
n.a = 10;
n.p = new int(10);
Node n1(n); // 这里执行拷贝操作
std::vector<Node>vec;
vec.push_back(std::move(n1)); // 这里执行move操作
std::cout << "n.str= " << n.str << std::endl;
std::cout << "n1.str= " << n1.str << std::endl;
std::cout << "vec.str= " << vec[0].str << std::endl;
return 0;
}
/*
输出结果:
default construction
copy construction
move construction
n.str= hello world !
n1.str=
vec.str= hello world !
deconstruction
deconstruction
deconstruction
*/
说明一点,std::move
完成后,原来的左值不会立刻析构,而是正常流程的结束并析构。
C++的std::forward
完美转发,在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。如果传入的参数是不是左值引用,那么返回一个参数右值的引用;如果参数是左值引用,那么返回左值的引用。
转换到右值引用使用std::move
,其余情况使用完美转发。
完美转发的一般用途:
template<typename T>
void IamForwording(T t) {
IrunCodeActually(t);
}
上面的代码模板说明,IamForwording
函数的用途只是把模板参数t传入进来,而IrunCodeActually
是真正执行的函数,该函数希望原封不动传递前者传入参数的类型。
为了处理各种参数的匹配关系,C++引入了参数折叠规则,给出编译推断的策略:
T& + & => T&
T&& + & => T&
T& + && => T&
T&& + && => T&&
+左侧是函数形参表示的形式,+右侧是实际传入参数的形式,=>后表示实际推断的形式。
虽然组合方式很多,但是有一个规律:只有形参和传入的参数同时是右值时,才会推断成右值引用;否则一律是左值。
因此,引入std::forward
来解决这个问题。
template <class T> T&& forward (typename remove_reference<T>::type& arg) noexcept;
template <class T> T&& forward (typename remove_reference<T>::type&& arg) noexcept;
函数的返回值是:如果arg
是左值,就返回左值;否则一律返回右值。
给出一个简单的例子:
给出C++参考的实例测试:
#include
#include
void overloaded(const int& x) {
std::cout << "lvalue\n";
}
void overloaded(int&& x) {
std::cout << "rvalue\n";
}
template<typename T>
void fn(T&& x) {
overloaded(x);
overloaded(std::forward<T>(x));
}
int main() {
int a;
std::cout << "calling fn with lvalue:\n";
fn(a);
std::cout << "calling fn with rvalue:\n";
fn(0);
return 0;
}
/*
输出结果:
calling fn with lvalue:
lvalue
lvalue
calling fn with rvalue:
lvalue
rvalue
*/
从结果可以看出,使用了std::forward
的函数参数才能原封不动的传递原来数据的类型。这样可以根据参数的类型,自动的进行不同的重载。