详解右值引用、移动构造、完美转发"/>
【C++11新特性】详解右值引用、移动构造、完美转发
博客主页:Skylar Lin
望本文能够给您带来一定的帮助,如果有错误的地方敬请斧正!
新人博主🧑,希望多多支持🍺,还有好多库存和大家分享🎁。
转载需注明出处和原作🌹。
前言
“右值引用”(Rvalue Reference)是 C++11 重要的新特性之一,搭配着 “移动构造” 和 “完美转发”,它们在C++的内存管理和函数调用方面发挥重要作用。在本文中,我总结了关于右值引用、移动构造和完美转发的一些常见用法,希望对大家有所帮助。
概念引入
目前为止,我们用过的所有引用都是左值引用——对左值的引用。
int var=42;
int& ref=var; // 创建一个var的引用
ref=99;
assert(var==99); // 原型的值被改变了,因为引用被赋值了
-
lvalue这个词来自于C语言,指的是可以放在赋值表达式左边的事物——在栈上或堆上分配的命名对象,或者其他对象成员——有明确的内存地址。
-
rvalue这个词也来源于C语言,指的是可以出现在赋值表达式右侧的对象——例如,文字常量和临时变量。
因此,左值引用只能被绑定在左值上,而不是右值。
例如,不能这样写,因为 42 是一个右值:
int& i=42; // 编译失败
好吧,这有些假;可以使用下面的方式将一个右值绑定到一个 const 左值引用上:
int const& i = 42;
这算是钻了标准的一个空子吧。不过这种情况也存在:
通过对左值的 const 引用创建临时性对象,作为参数传递给函数。其允许隐式转换,可以这样写:
void print(std::string const& s);
print("hello"); //创建了临时std::string对象
C++11标准介绍了右值引用(rvalue reference),这种方式只能绑定右值,不能绑定左值,其通过两个&&
来进行声明:
int&& i=42;
int j=42;
int&& k=j; // 编译失败
移动语义
右值通常都是临时的,所以可以随意修改;这就意味着,比起拷贝右值参数的内容,不如移动其内容。
举例
试想,一个函数以std::vector<int>
作为一个参数,并且不对原数组的数据做任何更改操作;
我们能够想到的方法是:将这个参数作为一个左值的 const 引用传入,然后做内部拷贝:
void process_copy(std::vector<int> const& vec_)
{
std::vector<int> vec(vec_);
vec.push_back(42);
}
这样写的话,允许函数能以左值或右值的形式进行传递,不过任何情况下都是通过拷贝来完成的,没有实际意义。
如果使用右值引用来重载这个函数,就能避免在传入右值的时候,函数会进行内部拷贝的过程,因为可以任意地对原始值进行修改:
void process_copy(std::vector<int> && vec)
{
vec.push_back(42);
}
移动构造函数
如果这个问题存在于类的构造函数中,窃取内部右值在新的实例中使用,那就是 “移动构造函数”。
学习移动构造函数前,先来看看原先的拷贝构造函数的弊端。
深拷贝构造函数
如果是拷贝构造函数进行的是深拷贝,可以实现对象独立性和数据隔离,但也可能引起性能开销和引入差异性。
-
深拷贝需要递归地复制整个对象结构,这可能导致性能开销较大,特别是当对象包含大量数据或嵌套层次较深时。相对于浅拷贝来说,深拷贝通常需要更多的时间和内存。
-
由于深拷贝会复制所有数据,如果原始对象中存在某种状态或关联,可能会导致拷贝后的对象与原始对象之间出现不一致,从而引入意料之外的错误。
浅拷贝构造函数
那么如果我们可以对指针进行浅拷贝,这样就避免了新的空间的分配,大大降低了构造的成本。
但是,指针的浅层复制是非常危险的,浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了,如果内存被重复释放,程序就会崩溃。
解决方法是:在拷贝构造函数中将原指针置为空,转移堆区内存资源的所有权。
#include <iostream>
#include <string>using namespace std;class Integer {
private:int* m_ptr;
public:Integer(int value) : m_ptr(new int(value)) {cout << "Call Integer(int value)有参" << endl;}//参数为常量左值引用的深拷贝构造函数,不改变 source.m_ptr 的值Integer(const Integer& source) : m_ptr(new int(*source.m_ptr)) {cout << "Call Integer(const Integer& source)拷贝" << endl;}//参数为左值引用的浅拷贝构造函数//转移堆内存资源所有权,source.m_ptr的值变为nullptrInteger(Integer& source) : m_ptr(source.m_ptr) {source.m_ptr= nullptr;cout << "Call Integer(Integer& source)" << endl;}~Integer() {cout << "Call ~Integer()析构" << endl;delete m_ptr;}int GetValue(void) { return *m_ptr; }
};Integer getNum()
{Integer a(100);return a;
}
int main(int argc, char const* argv[]) {Integer a(getNum()); cout << "a=" << a.GetValue() << endl;cout << "-----------------" << endl;Integer temp(10000);Integer b(temp);cout << "b=" << b.GetValue() << endl;cout << "-----------------" << endl;return 0;
}
到这里,好像问题都可以被解决,但是千万不能忘记:
-
浅拷贝仍然是一个危险的行为,可能导致资源共享和管理的问题;
-
拷贝过程中,数据的复制仍然是一种时间性能上的开销。
右值引用的移动构造函数
相比前两种拷贝构造函数,移动构造函数将资源从一个(临时)对象转移到另一个对象,避免不必要的数据复制。
移动构造函数的引入为资源管理和性能优化提供了更好的支持。
class MyResource {// Some resource management implementation...
};// 移动构造函数
MyResource::MyResource(MyResource&& other) {// 在这里执行资源移动操作,将other的资源移动到当前对象// 而不是进行深拷贝
}// 函数接受右值引用参数,执行资源的转移
void processResource(MyResource&& resource) {// 在这里使用右值引用参数,资源被移动到了这个函数内// 而不是进行数据复制
}int main() {MyResource tempResource; // 创建一个右值(临时对象)processResource(std::move(tempResource)); // 传递右值,并使用std::move将其转换为右值引用// 此时tempResource的资源已经被转移,不会导致资源的重复释放return 0;
}
将变量转换为右值的方法有两种:
-
调用
std::move()
函数,且需要包含<utility>
头文件; -
使用类型转换的运算符
static_cast
:static_cast<Class&&>()
。
MyClass obj1;
MyClass&& obj2 = std::move(obj1);
MyClass&& obj3 = static_cast<MyClass&&>(obj2);
注意:使用右值引用需要谨慎,特别是在函数中接受右值引用参数时,应该明确地表明调用者可能会失去对传递的右值的所有权。同时,确保在移动资源后,原来的右值不再被使用,以免出现悬空指针或访问已释放资源的情况。
右值引用与函数模板
通用引用
在使用右值引用作为函数模板的参数时,与之前的用法有些不同:
-
如果函数模板参数以右值引用作为一个模板参数,当对应位置提供右值的时候,会当做普通数据使用;
-
而当对应位置提供左值的时候,模板会自动将其类型认定为左值引用。
template<typename T>
void foo(T&& t)
{}// 传入右值时
foo(42); // foo<int>(42)
foo(3.14159); // foo<double><3.14159>
foo(std::string()); // foo<std::string>(std::string())// 传入左值时
int i = 42;
foo(i); // foo<int&>(i)
因为函数参数声明为T&&
,所以就是引用的引用,可以视为是原始的引用类型。那么foo()就相当于:
foo<int&>(); // void foo<int&>(int& t);
这就允许一个函数模板可以即接受左值,又可以接受右值参数;
而这种技术被称为 “通用引用”(Universal Reference)。
引用折叠
还有一个问题继续解决,即如果调用 foo() 函数时为其传递一个左值引用或者右值引用的实参,如下所示:
template<typename T>
void foo(T&& t)
{}int n = 10;
int & num = n;
foo(num); // T 为 int&int && num2 = 20;
foo(num2); // T 为 int &&
上述例子中,我们看到,foo(num);
中T
被推断为int&
,将推导类型int&
带入模板,得到void foo(int& && t);
,参数类型变为int& &&
;同理,对于foo(num2);
来说,参数类型则变为int&& &&
。
但是,如果当我们在IDE中敲下类似这样的代码:
// ...
int a = 0;
int &ra = a;
int & &rra = ra; // 编译器报错:不允许使用引用的引用!
// ...
既然像int& &
这样的类型不存在,那foo()
函数的参数类型是什么呢?
原来,C++11 引入了引用折叠规则,这些规则在模板参数类型推导时起作用,使得代码更加简洁、易于理解。
引用折叠规则如下:
-
如果任一引用为左值引用,则结果为左值引用(
int&
)。 -
否则(即两个都是右值引用),结果为右值引用(
int&&
)。
完美转发
好了,现在我们有了通用引用,也知道了引用折叠的规则。当我们既需要接收左值类型,又需要接收右值类型的时候,再也不用分开写两个重载函数了。那么,什么情况下,我们需要一个函数,既能接收左值,又能接收右值呢?
答案就是:转发的时候。我们来看一个例子:
#include <iostream>
using namespace std;// 通用引用,转发接收到的参数 param
template<typename T>
void PrintType(T&& param)
{f(param); // 将参数param转发给函数 void f()
}// 接收左值的函数 f()
template<typename T>
void f(T &)
{cout << "f(T &)" << endl;
}// 接收右值的函数f()
template<typename T>
void f(T &&)
{cout << "f(T &&)" << endl;
}int main(int argc, char *argv[])
{int a = 0;PrintType(a);//传入左值PrintType(int(0));//传入右值
}
我们执行上面的代码,按照预想,在main()
中我们给PrintType
分别传入一个左值和一个右值。PrintType
将参数转发给f()
函数。f()
有两个重载,分别接收左值和右值。
正常的情况下,PrintType(a);
应该打印f(T &)
,PrintType(int(0));
应该打印f(T &&)
。
但是,真实的输出结果是:
f(T &);
f(T &);
这又是为什么呢?
当外部传入参数给 PrintType
函数时,param既可以被初始化为左值引用,也可以被初始化为右值引用,取决于我们传递给 PrintType
函数的实参类型。但是,当我们在函数 PrintType
内部,将param
传递给另一个函数的时候,此时,param
是被当作左值进行传递的。
在任何的函数内部,对形参的直接使用,都是按照左值进行的。
想要改变这个情况,需要使用 std::forward
。
template <class T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept;template <class T>
T&& forward(typename std::remove_reference<T>::type&& t) noexcept;
具体的做法就是,将模板函数void PrintType(T&& param)
中对f(param)
的调用,改为f(std::forward<T>(param));
然后重新运行一下程序。输出如下:
f(T &);
f(T &&);
嗯,完美的转发!
本文中部分内容参考了网上其他博客,如有侵权联系修改。
更多推荐
【C++11新特性】详解右值引用、移动构造、完美转发
发布评论