C++右值引用

2023-12-18 18:41:54

一、温故而知新

在温故过程中,对于C++重载overload的函数匹配优先级有这样一个先后顺序:

g++ v4.8.1匹配规则;
完全匹配>常量匹配>升级转换>降级转换>省略号匹配

然后为了验证这个问题,我写了如下的示例代码:

#include <sal.h>
#define MYTRACE() do { std::cout << __FUNCTION__ << ",line" << __LINE__ << std::endl;}while(0)

class FnOverWrite
{
public:
    void ooxx(const int& a,const int& b){ MYTRACE();}

    void ooxx(const int&& a,const int&& b){MYTRACE();}    
    
    void ooxx(int a,double b){MYTRACE();}
    
  	void ooxx(double a,int b){MYTRACE();}
};


int main()
{
    FnOverWrite fow;
    int num1 = 3,num2 = 4;
    double d3 = 12.12,d4=0.001;

    // 调用左值引常用重载函数 ==》  void ooxx(const int& a,const int& b){ MYTRACE();}
    fow.ooxx(num1,num2);

	// 调用右值常引用重载函数 ==》void ooxx(const int&& a,const int&& b){MYTRACE();}    
    fow.ooxx(int(3),int(4)); 

    fow.ooxx(num1,d3);  /// 调用完全匹配 ==》 void ooxx(int a,double b){MYTRACE();}

    fow.ooxx(d3,(int)d4); /// 编译器傻眼,必须显示转换 ==》void ooxx(double a,int b){MYTRACE();}

    return 0;
}

那么,提个小问题,为什么fow.ooxx(int(3),int(4)); 调用的是右值引用?

二、浅尝辄止

右值引用是C++11引入的一个新特性,主要用于实现移动语义和完美转发。它有以下优点和缺点:

优点:

  • 实现移动语义:右值引用可以绑定到将要销毁的对象(例如临时对象或者使用std::move转换的对象)。这使得我们可以安全地“窃取”这些对象的资源,而不是复制它们。这可以大大提高性能,特别是对于大型对象。
  • 实现完美转发:使用右值引用,我们可以在模板函数中保持参数的原始类型(包括左值、右值、常量等属性)不变地传递给其他函数。这被称为完美转发。

缺点:

  • 代码复杂性:使用右值引用需要对C++的值类别有深入的理解。否则,可能会导致代码难以理解和维护。
  • 潜在的错误:如果不正确地使用右值引用,可能会导致难以发现的错误。例如,如果你从一个函数返回一个右值引用,那么返回的引用可能会引用一个已经销毁的对象。
  • 兼容性问题:右值引用是C++11引入的特性,不被旧版本的C++标准支持。如果你的代码需要在旧版本的C++编译器上运行,那么你不能使用右值引用。

注意事项:

在使用右值引用时,你需要注意以下几点:

  • 不要返回局部对象的右值引用:如果你从一个函数返回一个局部对象的右值引用,那么这个引用将会引用一个已经销毁的对象,这是未定义行为。
  • 谨慎使用 std::move:std::move 可以将一个左值转换为右值,从而触发移动语义。然而,一旦一个对象被 std::move,你就不能再使用这个对象了,除非你可以确定这个对象的状态。
  • 理解值类别:在使用右值引用时,你需要理解C++的值类别,包括左值、右值、纯右值(prvalue)和将亡值(xvalue)。这将帮助你理解何时应该使用右值引用,以及右值引用如何工作。
  • 理解移动语义和完美转发:移动语义和完美转发是右值引用的两个主要应用。你需要理解这两个概念,以及如何在你的代码中使用它们。

三、有的放矢

1、实现移动语义

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v1 = {1, 2, 3, 4, 5};
    std::vector<int> v2 = std::move(v1);  // 使用右值引用实现移动语义

    std::cout << "v1.size(): " << v1.size() << std::endl;  // 输出:v1.size(): 0
    std::cout << "v2.size(): " << v2.size() << std::endl;  // 输出:v2.size(): 5

    return 0;
}

在这个例子中,std::move(v1) 返回一个右值引用,这个右值引用绑定到 v1。然后,v2 的构造函数接收这个右值引用,并“窃取” v1 的资源,而不是复制它们。

2、实现完美转发

#include <iostream>
#include <utility>

void foo(int& x) { std::cout << "lvalue\n"; }
void foo(int&& x) { std::cout << "rvalue\n"; }

template <typename T>
void bar(T&& x) {
    foo(std::forward<T>(x));  // 使用右值引用实现完美转发
}

int main() {
    int i = 0;
    bar(i);  // 输出:lvalue
    bar(0);  // 输出:rvalue

    return 0;
}

在这个例子中,bar 函数接受一个右值引用参数,然后使用 std::forward(x) 将这个参数完美地转发给 foo 函数。

3、增加代码的复杂性

#include <iostream>
#include <utility>

template <typename T>
void swap(T& a, T& b) {
    T tmp(std::move(a));  // 使用右值引用
    a = std::move(b);     // 使用右值引用
    b = std::move(tmp);   // 使用右值引用
}



int main() {
    int x = 1;
    int y = 2;
    swap(x, y);

    std::cout << "x: " << x << ", y: " << y << std::endl;  // 输出:x: 2, y: 1

    return 0;
}

4、潜在的错误:不要返回局部对象的右值引用

#include <iostream>

int&& dangerous() {
    int x = 123;
    return std::move(x);  // 错误:返回对局部变量的右值引用
}

int main() {
    int&& r = dangerous();
    std::cout << r << std::endl;  // 未定义行为:r 引用了一个已经销毁的对象

    return 0;
}

在这个例子中,dangerous 函数返回一个对局部变量 x 的右值引用。当 dangerous 函数返回后,x 就被销毁了,所以 r 引用了一个已经销毁的对象,这是未定义行为。

5、 兼容性问题:

涉及到编译器和C++标准的版本。如果你的代码需要在旧版本的C++编译器上运行,那么你不能使用右值引用。

文章来源:https://blog.csdn.net/weixin_39568531/article/details/135063489
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。