Effective Modern C++ Item 23 理解std::move和std::forward

本文深入解析C++中的std::move与std::forward函数模板,揭示它们在移动语义中的角色与工作原理。std::move通过无条件类型转换创建右值,而std::forward则在特定条件下执行类型转换,保持参数的左值或右值特性。

std::move并不进行任何移动 std::forward 并不进行任何转发

这两者在运行期都没有任何作为,不会产生任何可执行代码,连一个字节都不会生成

std::movestd::forward 都仅仅是执行强制型别转换的函数(实际上是函数模板)

  • std::move 无条件将实参强制转换成右值。
  • std::forward仅在特点条件满足时,才执行同一个强制转换。

实际上本章所有内容就这么多,但如果要说的更具体些的话。

再开始之前,需要记住这样一点:所有形参都是左值,即便形参的型别是个指涉到T对象的右值引用,形参也是个左值。如果忘记了这一点,可以重温Item 1

std::move的本质只是个强转

以下实现虽然不完全符合标准的所有细节,但已经非常接近了:

template<typename T>                    //位于名字空间std内
typename remove_reference<T>::type&& 
    move(T&& param)
{
    using ReturnType = 
        typename remove_reference<T>::type&&;
    return static_cast<ReturnType>(param)     //  ①
}

实际上move的本质就是上述代码中的地方,其他的额外代码都是为了符合型别描述。std::move的形参是一个指涉到对象的万能引用,它返回的是指涉到同一个对象的引用。

上述为何要说是返回的是引用,注意返回值的&&部分,这里暗示返回值希望是一个右值。但如果T碰巧是个左值引用的话,T&&就成了左值引用。为了避免这种情况出现,代码中使用了std::remove_reference保证了&&一定是作用在非引用型别之上。从而确保了返回值一定是右值引用。所以std::move的本质就只是一个强转而已。

而上述代码在C++14中写法更为简单:

template<typename T>
decltype(auto) move(T&& param)
{
    using ReturnType = remove_reference_t<T>&&;
    return static_cast<ReturnType>(param);
}

其实在std::move命名之前,有人说这个函数更好的命名可能叫rvalue_cast。这个的确更贴切,但是既然这么叫了,也无妨。

std::move也只是在通常情况下能够移动

考虑这样一个场景:

class Annotation {
public:
    explicit Annotation(std::string text);  //待复制的形参
    ...
};

而对于只读对象,不需要修改,那么遵循着由来已久的传统:只要有可能使用const就使用const,你修改了代码变成这样:

class Annotation {
public:
    explicit Annotation(const std::string text);  //待复制的形参
    ...
};

为了避免付出将test复制入数据成员过程中产生的复制操作成本,又将text实施了std::move,从而产生了一个右值。为了防止不必要的默认初始化,聪明的你采用了构造函数初始化列表的形式。

class Annotation {
public:
    explicit Annotation(const std::string text)  //待复制的形参
    : value(std::move(text)) { ... }
    ...
};

这段代码顺利的通过了编译,完成了链接,也跑了起来,体现也看起来都正确,但美中不足的是,这里的text并非是被移动的,它依旧是被复制进value的。

以上代码逐步分析

的确,text已经被std::move强转成了一个右值。但是,text被声明为const std::string,所以在强转之前,是个左值const std::string,而强转后变为右值的const std::string

接着看,std::string的构造函数:

class string {                  //std::string实际上是个typedef
public:                         //代表std::basic_string<char>
    ...
    string(const string& rhs);  //复制构造函数
    string(string&& rhs);       //移动构造函数
}

在Annotation的构造函数的成员初始化列表中,std::move(text)的结构是个const std::string型别的右值。这个右值无法传递给std::string的移动构造函数,因为移动构造函数只能接受非常量的std::string型别的右值引用作为形参。可是这样一个右值可以传递给复制构造函数,因为指涉到常量的左值引用允许绑定到一个常量右值型别的形参。

因此,成员初始化最终会调用的是std::string的复制构造函数,即便text已经是一个右值。其实简单来说也好理解,move操作会改变该对象,而const对象是不允许被改变的

分析得到两点经验:
  1. 如果想对某个对象执行移动操作,不要将其声明为const,声明后会一声不响的调用了复制构造而不通知你。

  2. std::move不仅不实际移动任何东西,甚至不保证经过其强转型别后的对象具备可移动能力。std::move操作后唯一可以确定的是,该操作返回值是个右值。

std::forward分析

一言以蔽之:

std::forward是一个有条件的强制转换。

来个例子:

void process(const Widget& lvalArg);    //处理左值
void process(Widget&& lvalArg);         //处理右值

template<typename T>
void logAndProcess(T&& param)           //把param传递给process的函数模板
{
    auto now = 
        std::chrono::system_clock::now();   //获取当前时间
    makeLogEntry("Calling 'process'", now);
    process(std::forward<T>(param));
}

考虑两种调用logAndProcess的情景,一种传递左值,一种传入右值:

Widget w;

logAndProcess(w);                   //传入左值
logAndProcess(std::move(w));        //传入右值

上述代码中,我们肯定希望传入的左值w的时候,调用左值版本的process,传入右值w的时候,调用右值版本的process

但是,如开篇所说,所有函数形参皆为左值,param也是一样。所以,所有logAndProcess内,对process的调用都会是取用了左值型别的那个重载版本。为了避免这种结果,就需要一种机制,当且仅当用来初始化param的实参是个右值的条件下,把param强制转换成右值型别。

以上的这些描述,就是std::forward的功能:仅当其实参是使用右值完成初始化时,才会执行向右值型别的强制转换

std::forward机制简析

std::forward是通过传入的T来判断传入的是左值还是右值,并将这个信息转发下去的。

std::forwardstd::move的小插曲

既然这两者都是强制转换,并且std::forward的功能仅仅有时会实施。这么一来你不免会问,是否可以弃用std::move而只用std::forward。纯粹从技术的角度来说是可以的,甚至纯粹从技术的角度看,这两个函数没有一个是必不可少的。因为毕竟都可以自己写强转。但依旧需要记住:

  • std::move是无条件强转,本质是为了移动操作做铺垫。

  • std::forward是有条件转换,本质是为了保证传入和输出的变量左右值属性不改变。

要点速记
1. std::move试试的是无条件的向右值型别转换,它本身是不会执行移动操作的。
2. 仅当传入的实参被绑定到右值时,std::forward才针对该实参实施向右值型别的强制型别转换。
3. 在运行期,std::move和std::forward都不会做任何操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值