C++左值与右值,移动与完美转发

版权声明:所有的博客都是作为个人笔记的。。。。。。 https://blog.csdn.net/qq_35976351/article/details/82820834

左值与右值

判别:

  • 左值:用来存储数据的变量,有实际的内存地址,表达式结束后任然存在。
  • 右值:匿名的临时变量,表达式结束时被销毁,不能存放数据,可以被修改或者不修改;字面常量也是右值。
int x=10;
int* p=&++x;  // 正确,前置++返回左值
int* q=&x++;  // 错误,后置++返回临时对象

左值引用就是普通的引用,下面介绍右值引用:

#include <iostream>
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()进行转移对象,把一个对象(左值)转移成匿名的右值。它的意义在于减少不必要的拷贝操作,提高程序性能。本质上,std::move是使得对象的所有权发生了转移,给出代码实例

#include <iostream>
#include <vector>
#include <string>

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 <iostream>
#include <vector>
#include <string>

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完成后,原来的左值不会立刻析构,而是正常流程的结束并析构。

std::forward完美转发

C++的std::forward完美转发,在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。如果传入的参数是不是左值引用,那么返回一个参数右值的引用;如果参数是左值引用,那么返回左值的引用。

完美转发的一般用途:

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 <utility>
#include <iostream>

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的函数参数才能原封不动的传递原来数据的类型。这样可以根据参数的类型,自动的进行不同的重载。

猜你喜欢

转载自blog.csdn.net/qq_35976351/article/details/82820834