首页 > 编程语言 >C++的std::move与std::forward原理总结

C++的std::move与std::forward原理总结

时间:2023-10-27 11:03:54浏览次数:51  
标签:std 右值 move 左值 C++ 引用 &&

目录

  阅读大型的C++开源项目代码,基本逃不过std::movestd::forward,例如webRTC
所以搞懂其原理,很有必要。

0、左值与右值的理解

左值和右值的概念

  C++中左值(lvalue)和右值(rvalue)是比较基础的概念,虽然平常几乎用不到,但C++11之后变得十分重要,它是理解 move/forward 等新语义的基础。

  左值与右值这两个概念是从 C 中传承而来的,左值指既能够出现在等号左边,也能出现在等号右边的变量;右值则是只能出现在等号右边的变量。

int a;		// a 为左值
a = 3;		// 3 为右值
  • 左值是可寻址的变量,有持久性;
  • 右值一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的。

左值和右值主要的区别之一是左值可以被修改,而右值不能。

左值引用和右值引用

  • 左值引用:引用一个对象;
  • 右值引用:就是必须绑定到右值的引用,C++11中右值引用可以实现“移动语义”,通过 && 获得右值引用。
int x = 6;					// x是左值,6是右值
int &y = x;					// 左值引用,y引用x

int &z1 = x * 6;			// 错误,x*6是一个右值
const int &z2 =  x * 6;		// 正确,可以将一个const引用绑定到一个右值

int &&z3 = x * 6;			// 正确,右值引用
int &&z4 = x;				// 错误,x是一个左值

  右值引用和相关的移动语义是C++11标准中引入的最强大的特性之一,通过std::move()可以避免无谓的复制,提高程序性能。

1. std::move

  别看它的名字叫 move,其实std::move并不能移动任何东西,它唯一的功能是将一个左值/右值强制转化为右值引用,继而可以通过右值引用使用该值,所以称为移动语义

  std::move的作用:将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能。 它是怎么个转移法,将在文章的最后面解释。

  看到std::move的代码,意味着给std::move的参数,在调用之后,就不再使用了。

1.1 函数原型

/**
 *  @brief  Convert a value to an rvalue.
 *  @param  __t  A thing of arbitrary type.
 *  @return The parameter cast to an rvalue-reference to allow moving it.
 */
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {
	return static_cast<typename remove_reference<T>::type&&>(t);
}

用到的remove_reference定义

/// remove_reference
  template<typename _Tp>
    struct remove_reference
    { typedef _Tp   type; };

  template<typename _Tp>
    struct remove_reference<_Tp&>
    { typedef _Tp   type; };

  template<typename _Tp>
    struct remove_reference<_Tp&&>
    { typedef _Tp   type; };

1.2 参数讨论

  先看参数 T&& t,其参数看起来是个右值引用,其是不然!!!

  因为 T 是个模板,当右值引用和模板结合的时候,就复杂了。T&&并不一定表示右值引用,它可能是个左值引用又可能是个右值引用。

再弄个清爽的代码解释一下:

template<typename T>
void func( T&& param){
    
}
func(5);		// 5是右值,param是右值引用
int a = 10;
func(a);		// x是左值,param是左值引用

  这里的&&是一个未定义的引用类型,称为通用引用 Universal References(https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers)

  它必须被初始化,它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。

  注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个Universal References。

1.3 通用引用

  这里还可以再深入一下通用引用,解释为什么它一会可以左值引用,一会可以右值引用。

  既然 T 是个模板,那 T 就可以是 string ,也可以是 string& ,或者 string&& 。

  那参数就变成string&& && param了,这么多 & 怎么办?好吓人!!!没事,稳住,C++ 11立了规矩,太多&就要折叠一下(也就是传说中的引用折叠)。具体而言

X& &、X&& &、X& &&都折叠成X&

X&& &&折叠成X&&

  所以,想知道 param 最终是什么引用,就看 T 被推导成什么类型了。
可以用下面的一个测试程序,来验证。

#include <iostream>
#include <type_traits>
#include <string>
using namespace std;
template<typename T>
void func(T&& param) {
    if (std::is_same<string, T>::value)
        std::cout << "string" << std::endl;
    else if (std::is_same<string&, T>::value)
        std::cout << "string&" << std::endl;
    else if (std::is_same<string&&, T>::value)
        std::cout << "string&&" << std::endl;
    else if (std::is_same<int, T>::value)
        std::cout << "int" << std::endl;
    else if (std::is_same<int&, T>::value)
        std::cout << "int&" << std::endl;
    else if (std::is_same<int&&, T>::value)
        std::cout << "int&&" << std::endl;
    else
        std::cout << "unkown" << std::endl;
}

int getInt() { return 10; }

int main() {
    int x = 1;
    // 传递参数是右值 T 推导成了int, 所以是 int&& param, 右值引用
    func(1);
    // 传递参数是左值 T 推导成了int&, 所以是int&&& param, 折叠成 int&,左值引用
    func(x);
    // 参数getInt是右值 T 推导成了int, 所以是int&& param, 右值引用
    func(getInt());
    return 0;
}

1.4 返回值

  以 T 为 string 为例子,简化一下函数定义:

// T 的类型为 string
// remove_reference<T>::type 为 string 
// 整个std::move 被实例如下
string&& move(string&& t) {					// 可以接受右值
    return static_cast<string&&>(t);		// 返回一个右值引用
}

  显而易见,用static_cast,返回的一定是个右值引用。

综上,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);

即,输入可以是左值,右值,输出,是一个右值引用。

1.5 std::move的常用例子

1.5.1 用于vector添加值

以下是一个经典的用例:

// 摘自https://zh.cppreference.com/w/cpp/utility/move
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main() {
    std::vector<std::string> v;
    std::string str = "Hello";
    // 调用常规的拷贝构造函数,新建字符数组,拷贝数据
    v.push_back(str);
   	std::cout << "After copy, str is \"" << str << "\"\n";
    //调用移动构造函数,掏空str,掏空后,最好不要使用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";
}
// 输出
// After copy, str is "Hello"
// After move, str is ""
// The contents of the vector are "Hello", "Hello"

1.5.2 用于unique_ptr传递

#include <stdio.h>
#include <unistd.h>
#include <memory>
#include <iostream>

/***** 类定义开始****/
class TestC {
public:
    TestC(int tmpa, int tmpb):a(tmpa),b(tmpb) {
        std::cout<< "construct TestC " << std::endl;
    }
    ~TestC() {
        std::cout<< "destruct TestC " << std::endl;
    }
    void print() {
        std::cout << "print a " << a << " b " << b << std::endl;
    }
private:
    int a = 10;
    int b = 5;
};
/***** 类定义结束****/
void TestFunc(std::unique_ptr<TestC> ptrC) {
    printf("TestFunc called \n");
    ptrC->print();
}
int main(int argc, char* argv[]) {
    std::unique_ptr<TestC> a(new TestC(2, 3));
    // 初始化也可以写成这样
    // std::unique_ptr<TestC> a = std::make_unique<TestC>(2, 3);
    TestFunc(std::move(a));
    // 执行下面这一句会崩溃,因为a已经没有控制权
    a->print();
    return 0;
}
// 执行后输出
// construct TestC 
// TestFunc called 
// print a 2 b 3
// destruct TestC 

  从日志可见,只有一次构造。

  这种类型的代码,在大型开源项目,如 webRTC ,随处可见。下次看到了不用纠结,不用关心细节了。只要直到最后拿到 unique_ptr 的变量(左值)有控制权就行了。

1.6 再说转移对象控制权

  从上面1.5.2的例子,看到std::move(a)之后,执行a->print();后会崩溃,这是为什么呢?
  其实不全部是std::move的功劳,还需要使用方,即unique_ptr配合才行。

请看这篇文章:https://blog.csdn.net/newchenxf/article/details/116274506

当调用TestFunc(std::move(a));TestFunc的参数 a 要初始化,调用的是operator=,关键代码截取如下:

class unique_ptr {
private:
	T* ptr_resource = nullptr;
	...
	
    // "="号赋值操作,此处的 move 即为 a
    unique_ptr& operator=(unique_ptr&& move) noexcept {
		move.swap(*this);  // 置换后 a 就变成空了,丢失了原来的控制权
		return *this;
	}
    
	// 交换函数
	void swap(unique_ptr<T>& resource_ptr) noexcept {
		std::swap(ptr_resource, resource_ptr.ptr_resource);  // 这里把空换给了 a 
	}

  从operator=函数看,执行完赋值后,智能指针的托管对象即 ptr_resource,交换了本来函数的参数 a ,托管对象 ptr_resource 为空,现在换来了一个有用的,把空的换给了a,于是 a 的资源为空,所以 a 使用资源时就会出现空指针的错误!

2. std::foward

  有了前面的讨论,这个就简单一些了,不铺的很开。先看函数原型:

/**
 *  @brief  Forward an lvalue.
 *  @return The parameter cast to the specified type.
 *  This function is used to implement "perfect forwarding".
 */
template<typename _Tp>
constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept { 		return static_cast<_Tp&&>(__t); 
}

  /**
   *  @brief  Forward an rvalue.
   *  @return The parameter cast to the specified type.
   *  This function is used to implement "perfect forwarding".
   */
template<typename _Tp>
constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type&& __t) noexcept {
	static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument substituting _Tp is an lvalue reference type");
	return static_cast<_Tp&&>(__t);
}

有两个函数:

  第一个,参数是左值引用,可以接受左值。

  第二个,参数是右值引用,可以接受右值。

  根据引用折叠的原理,如果传递的是左值,Tp 推断为 string& ,则返回变成static_cast<string& &&>,也就是 static_cast<string&>,所以返回的是左值引用。

  如果传递的是右值,Tp 推断为 stringstring&& ,则返回变成
static_cast<string&&>,所以返回的是右值引用。

  反正不管怎么着,都是一个引用,那就都是别名,也就是谁读取std::forward,都直接可以得到std::foward所赋值的参数。这就是完美转发的基本原理!

/**
 * 编译:g++ test_forward.cpp -lpthread -o out
 * 执行:./out
 * 这是测试代码,不够严谨,仅为了说明std::forward的用途
 * 例子的意思是,希望执行一个函数,函数放在子线程执行,函数由业务方随时定义
 * */
#include <stdio.h>
#include <unistd.h>
#include <memory>
#include <iostream>
#include <thread>

template <typename Closure>
class ClosureTask {
public:
    explicit ClosureTask(std::string &&name, Closure &&closure):
        name_(std::forward<std::string>(name)),
        closure_(std::forward<Closure>(closure)) {
        }
    
    bool DoTask() {
        // 执行Lambda函数
        closure_();
        return true;
    }
private:
    typename std::decay<Closure>::type closure_;
    std::string name_;
};

// 异步调用,非阻塞
template <typename Closure>
void PostTask(std::string &&name, Closure &&closure) {
    std::unique_ptr<ClosureTask<Closure>> queueTask(
        // 用 forward 透传 name
        new ClosureTask<Closure>(std::forward<std::string>(name),
        // 用 forward 透传 closure
        std::forward<Closure>(closure)));
    printf("PostTask\n");
    // 启动一个线程执行任务,taskThread的第二个参数,也是一个Lambda表达式
    // =号表示外部的变量都可以在表达式内使用, &queueTask表示表达式内部要使用该变量
    std::thread taskThread([=, &queueTask]() { 
        printf("start thread\n");
        queueTask->DoTask();
        printf("thread done\n");
    });
    taskThread.detach();
}

int main(int argc, char* argv[]) {
    printf("start\n");
    // 参数2,传递的是Lambda表达式
    // Lambda 是最新的 C++11 标准的典型特性之一。Lambda 表达式把函数看作对象
    PostTask("TestForward", []() mutable {
        // 执行一个任务,任务的内容就在这里写
        printf("I want to do something here\n");
    });
    return 0;
}

参考

本文的原文地址

c++ 之 std::move 原理实现与用法总结

[c++11]我理解的右值引用、移动语义和完美转发

标签:std,右值,move,左值,C++,引用,&&
From: https://www.cnblogs.com/hhddd-1024/p/17791237.html

相关文章

  • C++ invoke与function的区别
    C++invokeinvoke是C++17标准引入的一个函数模板,用来调用可调用对象(CallableObject,如函数指针、函数对象、成员函数指针等)并返回结果。invoke提供了统一的调用语法,无论可调用对象的类型是什么,都可以使用同一种方式进行调用。详见:https://en.cppreference.com/w/cpp/utility/fu......
  • C++运算符
    C++运算符运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C++内置了丰富的运算符,并提供了以下类型的运算符:算术运算符关系运算符逻辑运算符位运算符赋值运算符杂项运算符算术运算符下表显示了C++支持的所有算术运算符。假设变量A=10;B=20,则:运算符描......
  • murmurhash64B c# 实现 c++ 实现
    c#实现:usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;namespacegjh.utility{publicclassMurmurHash64B{publicstaticulongMakeHashValue(byte[]key,uintseed=0xee6b27eb){......
  • C++ 与 QML 之间进行数据交互的几种方法
    一、属性绑定这是最简单的方式,可以在QML中直接绑定C++对象的属性。通过在C++对象中使用Q_PROPERTY宏定义属性,然后在QML中使用绑定语法将属性与QML元素关联起来。person.h#include<QObject>classPerson:publicQObject{Q_OBJECT/*使用Q_PROPERTY定义交......
  • c++中的继承(下)
    首先我们先回忆一下,在派生类(子类)中默认的成员函数做了什么事情?我们现在可以这么认为对于普通类来说呢?只需要看待两个部分的成员:内置类型和自定义类型。而对于派生类而言序言看待三个部分的成员:内置类型,自定义类型以及父类类型构造和析构拷贝构造普通类对于内置类型一般不处理,自定类......
  • C++修饰符类型
    C++允许在char、int和double数据类型前放置修饰符。修饰符用于改变基本类型的含义,所以它更能满足各种情境的需求。当前有以下几种数据类型修饰符:signedunsignedlongshort修饰符signed、unsigned、long和short可应用于整型,signed和unsigned可应用于字符型,long可应用于双精度......
  • 【每日例题】 蓝桥杯 c++ 考勤刷卡
    考勤刷卡题目小蓝负责一个公司的考勤系统,他每天都需要根据员工刷卡的情况来确定每个员工是否到岗。当员工刷卡时,会在后台留下一条记录,包括刷卡的时间和员工编号,只要在—天中员工刷过—次卡,就认为他到岗了。现在小蓝导出了—天中所有员工的刷卡记录,请将所有到岗员工的员工......
  • 现代C++语言核心特性解析 谢丙堃​ 2021年pdf电子版
    现代C++语言核心特性解析2021年pdf电子版作者: 谢丙堃出版年: 2021-10ISBN: 9787115564177连接提取码:ckop自从C++11发布,就没有系统学习C++的书,也很久没有看国内作者出的C++书籍了。市面上对于现代C++的书很少,这是一本讲述现代C++11~C++20的书。意外,写得不错,容易理解,难得是除了......
  • C++中vector容器详解
    参考链接:https://www.runoob.com/w3cnote/cpp-vector-container-analysis.html一、什么是vector?向量(Vector)是一个封装了动态大小数组的顺序容器(SequenceContainer)。跟任意其它类型容器一样,它能够存放各种类型的对象。可以简单的认为,向量是一个能够存放任意类型的动态数组。二......
  • 引用C++程序,在DOS命令行打印彩色玫瑰花
    python代码:fromctypesimport*importpygameimportrandomimportstringimporttimeif__name__=='__main__':withopen('log.txt','rb')asf:lines=f.readlines()count=0forlineinlines:......