【C++11新特性】详解右值引用、移动构造、完美转发

编程入门 行业动态 更新时间:2024-10-07 22:26:18

【C++11新特性】<a href=https://www.elefans.com/category/jswz/34/1770044.html style=详解右值引用、移动构造、完美转发"/>

【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_caststatic_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新特性】详解右值引用、移动构造、完美转发

本文发布于:2024-03-14 00:32:20,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1735227.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:详解   新特性   完美

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!