c++:引用,移动,和转发

引用

左值和右值

左值:lvalue,一个在内存中占有确定位置的对象(换句话说就是有一个地址)
右值:与左值相反,没有内存位置,临时对象
比如下面的代码:

int i = 4;

i为左值,4为右值,因此
下面是错误的:

4 = i;
左值引用和右值引用

左值引用:对左值的引用,保存左值的地址
右值引用:对右值的引用
如下所示:

int a = 4;
int& b = a; // 将左值a绑定到左值引用b上
int&& c= a + 4; // 将右值a+4绑定到右值引用c上

下面是错误的:

int a = 4;
int &b = a + 4; // 错误, a+4是右值
int &&c = a; // 错误,a是左值

但是右值引用有什么作用呢?

右值引用的作用

根据右值的定义,可以推断出右值有以下特性:

  • 所引用的对象将要被销毁
  • 该对象没有其他用户
    所以,右值引用的作用便是 当对象的资源不再被原先对象需要时,可以唯一由右值引用负责。换句话说,
    使用右值引用的代码可以自由的接管被引用对象的资源。

移动

上面代码举例中展示了左值不能绑定到右值引用上,但是可以通过move函数,显示的将一个左值转化为对应的右值引用,并剥夺原先左值占用对象的权力,如下所示:

string st = "I love xing";
    vector<string> vc ;
    vc.push_back(move(st));
    cout<<vc[0]<<endl; // 不输出任何东西
    if(!st.empty()) 
        cout<<st<<endl; // 输出 I love xing
移动构造函数

拷贝构造函数不同的是,移动构造函数接受的是右值引用而非左值引用,并且经过移动构造函数,被移动的对象的资源将被”窃取“掉。在完成资源的移动之后,源对象将不在拥有任何资源,其资源所有权已经转交给新创建的对象。
举例如下:

#include <cstring>
#include <iostream>

class MyString {
 private:
  char* string;

 public:
  MyString() : string(nullptr) {}

  MyString(const char* str) {
    // 这里采用深拷贝
    string = (char*)malloc(strlen(str) + 1);
    strcpy(string, str);
    std::cout << "I'm constructor of class MyString" << std::endl;
  }

  MyString(const MyString& mystr) {
    // 这里同样是深拷贝,mystr 任然持有它自己的资源
    string = (char*)malloc(strlen(mystr.string) + 1);  // 为 string 分配新的资源
    strcpy(string, mystr.string);
    std::cout << "I'm copy constructor of class MyString" << std::endl;
  }

  MyString(MyString&& mystr) noexcept : string(mystr.string) {
    // 注意!移动构造函数,这里 mystr 已经不再持有任何资源
    // mystr.string 所指向的资源已经被当前对象窃取
    // 这里切记要将被移动的资源的指针置为空,为了防止析构函数析构其已经被转移的资源
    mystr.string = nullptr;
    std::cout << "I'm move constructor of class MyString" << std::endl;
  }

  ~MyString() {
    std::cout << "I'm destructor of class MyString" << std::endl;
    if (string) {
      // 如果 string 指针还持有资源的话,就将其释放
      free(string);
      std::cout << "free string!" << std::endl;
    }
  }
};

int main1(int argc, char* argv[]) {
  MyString s1("hello world");
  MyString s2(s1); // 调用MyString(const MyString& mystr)
  //MyString s3(std::move(s1));  // 调用MyString(MyString&& mystr) noexcept : string(mystr.string)
  std::cout << std::endl;
  return 0; // 函数退出时两次析构,两次free
}

int main2(int argc, char* argv[]) {
  MyString s1("hello world");
  //MyString s2(s1); // 调用MyString(const MyString& mystr)。
  MyString s3(std::move(s1));  // 调用MyString(MyString&& mystr) noexcept : string(mystr.string)
  std::cout << std::endl;
  return 0; // 函数退出时两次析构,一次free
}
注意

如果没有定义移动构造函数,但是实际代码中用到了move,则默认会调用类的拷贝构造函数;
如果定了移动构造函数而没有定义拷贝构造函数,则编译器不会自动生成拷贝构造函数,即相当于把拷贝构造函数定义为=delete;

转发

在我们调用函数的时候,会把实参传递给函数,有时候我们传给函数的是左值,有时候给的是右值,有时候还可能给的是 const 类型。在这些情况下,我们要求函数接收参数后,依然能保持这些类型,这时候就需要用转发。
比如下面的例子:

// 首先定义一个函数模板
template <typename F, typename T1, typename T2>
void middle1(F f, T1 t1, T2 t2) {
  f(t1, t2);
}

// 定义一个函数
void f(int v1, int& v2) {  // v2 是一个引用
  ++v1;
  ++v2;
}

// main函数中直接调用f(int v1, int& v2) 通过函数模板middle1(F f, T1 t1, T2 t2)调用f
int main(int argc, char* argv[]) {
  int i = 0;
  f(42, i);
  cout << "After call the f directly: " << i << endl; // 输出 1
  middle1(f, 42, i);
  cout << "Call f through middle1: " << i  << endl; // 输出 1
}

上面的例子可以看出,通过函数模板调用f,i并没有被+1,当我们将 i 绑定到 middle 的参数 t2 上后,传给 f 的是 t2,而 t2 只是一个普通的、非引用的类型 int,而不是对 i 的引用。所以 i 的值并没有改变。

保留类型信息

通过将函数模板定义如下,便可以实现i继续+1

template <typename F, typename T1, typename T2>
void middle2(F f, T1&& t1, T2&& t2) {
  f(t1, t2);
}

这样便可以实现i自增两次。在 middle2 中,实参的“左值性“得到了保留。这就要归功于 引用折叠 了,简单说一下引用折叠:

  • X& &、X& && 和 X&& & 都会折叠成类型 X&

  • 类型 X&& && 折叠成 X&&

但是考虑如下函数:

void g(int &&v1, int& v2) {
  ++v1;
  ++v2;
}
int main(int argc, char* argv[]) {
  int i = 0;
  middle2(g, 42, i); // 会报错,无法将一个右值引用绑定到左值上。
  cout << "Call g through middle2: " << i << endl;
}

为什么编译器会提示无法将一个右值引用绑定到左值上?因为 g 的参数 v1 是右值引用,将 42 给右值引用完全没问题,但问题就出在,虽然 42 是右值,但 t1 却不是,变量都是左值,即使是右值引用,但也是变量,所以还是左值,而左值是无法与右值引用绑定,即发生了如下的错误:

int&& a = 4;
int&& b = a; // 错误,a为右值引用,但是也是一个变量,是一个左值。
std::forward函数

foward 要明确给出模板参数:std::foward(i) 才能使用。forward 会返回模板参数类型的右值引用,也就是:int&& 。注意,如果我们这样使用:std::forward<int&>(i) 返回的就是 int& && ,进而折叠为 int& 。
因此,对于上面的g函数,可以通过以下方法保留类型信息:

template <typename F, typename T1, typename T2>
void middle3(F f, T1&& t1, T2&& t2) {
  f(std::forward<T1>(t1), std::forward<T2>(t2));
}

void g(int&& i, int& j) {
  ++i;
  ++j;
}

int main(int argc, char* argv[]) {
  int i = 0;
  middle3(g, 42, i);
  cout << "Call g through middle3: " << i << endl; // 输出 2
}

上面两个函数参数发生的引用折叠为:
std::forward(t1):int&& && -> int&&
std::forward(t2):int& && -> int&
可以看出,左值引用和右值引用都得到了保留。

ref

  • https://blog.csdn.net/lws123253/article/details/80353197
  • https://guodong.plus/2020/0314-132811/
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值