std::move并不进行任何移动std::forward并不进行任何转发
这两者在运行期都没有任何作为,不会产生任何可执行代码,连一个字节都不会生成。
std::move和std::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对象是不允许被改变的。
分析得到两点经验:
-
如果想对某个对象执行移动操作,不要将其声明为const,声明后会一声不响的调用了复制构造而不通知你。
-
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::forward和std::move的小插曲
既然这两者都是强制转换,并且std::forward的功能仅仅有时会实施。这么一来你不免会问,是否可以弃用std::move而只用std::forward。纯粹从技术的角度来说是可以的,甚至纯粹从技术的角度看,这两个函数没有一个是必不可少的。因为毕竟都可以自己写强转。但依旧需要记住:
-
std::move是无条件强转,本质是为了移动操作做铺垫。 -
std::forward是有条件转换,本质是为了保证传入和输出的变量左右值属性不改变。
| 要点速记 |
|---|
| 1. std::move试试的是无条件的向右值型别转换,它本身是不会执行移动操作的。 |
| 2. 仅当传入的实参被绑定到右值时,std::forward才针对该实参实施向右值型别的强制型别转换。 |
| 3. 在运行期,std::move和std::forward都不会做任何操作。 |
本文深入解析C++中的std::move与std::forward函数模板,揭示它们在移动语义中的角色与工作原理。std::move通过无条件类型转换创建右值,而std::forward则在特定条件下执行类型转换,保持参数的左值或右值特性。
1590

被折叠的 条评论
为什么被折叠?



