前言
在开始叙述之前,我们首先需要明确,std::move并不执行移动,std::forward也并不转发,它们在运行期并不执行任何操作,也不生成任何代码。
std::move和std::forward只是执行强制转换的函数(实际上是函数模板)。std::move无条件地将其参数转换为rvalue,而std::forward仅在满足特定条件时才执行此转换,这就是关于它们的全部解释。
std::move
C++11中std::move的大致实现如下所示:1
2
3
4
5
6template<typename T> // in namespace std
typename remove_reference<T>::type&&
move(T&& param){
using ReturnType = typename remove_reference<T>::type&&;
return static_cast<ReturnType>(param);
}
函数返回类型的“&&”部分意味着std::move返回一个右值引用,但正如Item28将要提及的,如果类型T碰巧是左值引用,则T&&将成为左值引用。为了防止这种情况发生,将type_traits(见Item9)std::remove_reference应用于T,从而确保将“&&”应用于不是引用的类型,这一操作保证std::move真正返回一个rvalue引用,这一点相当重要,因为函数返回的右值引用是一个右值,最终,move将其参数转为了一个右值。
在C++14中,move的实现可以更加简单(多亏了返回类型推衍与type_traits的福):1
2
3
4
5template<typename T> // C++14
decltype(auto) move(T&& param){
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}
move问题实例
假设我们正在撰写一个表示注释的类,其构造函数参数为string,按照Item41的要求,我们将其设定为pass-by-value:1
2
3
4class Annotation {
public:
explicit Annotation(std::string text);
}
这时候你想到构造函数不会改变text,于是你把它声明为const:1
2
3
4
5class Annotation {
public:
explicit Annotation(const std::string text)
…
};
为了避免对data member作无谓的复制,你在构造函数中使用std::move(亦遵从Item 41):1
2
3
4
5
6
7class Annotation {
public:
explicit Annotation(const std::string text): value(std::move(text)){ … } // doesn't do what it seems to!
…
private:
std::string value;
};
这段代码能跑,只是运行起来未必如你所愿,事实上它执行了copy而非move。text被强制转换为右值这一点没毛病,但由于text被声明为const string,因此在执行转换之前,text是一个左值const std::string,并且转换完成后它变成了一个右值const std::string,在转换过程中,const一直被保留着。
编译器需要考虑使用哪种string构造函数,一般来说有两种选择:1
2
3
4
5
6
7class string {
public:
…
string(const string& rhs); // copy ctor
string(string&& rhs); // move ctor
…
};
在Annotation构造函数的member initialization list中,std::move(text)的结果是const std::string类型的rvalue。该rvalue无法传递给st::string的移动构造函数,因为移动构造函数形参必须是一个rvalue non-const std::string。但该rvalue可以传递给复制构造函数,因为允许将lvalue-reference-to-const绑定到const rvalue。 因此,成员初始化调用std::string中的复制构造函数。理解这一点十分重要,因为移动构造必然会破坏某些对象,因此移动构造的参数不具备const属性。
从该实例中可以得出两条经验:
- 如果希望move某对象,不要将其声明为const,这将导致只会触发copy。
- std::move并不保证对象被移动,它只能保证传递回一个右值而已。
std::forward
forward是一种条件式转换,仅仅在满足条件时才会将参数转为rvalue。要了解它何时进行转换以及何时不转换,只需要想想我们一般在何时使用std::forward。
问题实例
最常见的场景是一个函数模板,其形参为universal reference,该参数将被传递给另一个函数:1
2
3
4
5
6
7
8void process(const Widget& lvalArg); // process lvalues
void process(Widget&& rvalArg); // process rvalues
template<typename T> // template that passes
void logAndProcess(T&& param){
auto now = std::chrono::system_clock::now();
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param));
}
考虑下述两个函数调用,其中一个实参是左值,另一个是右值:1
2
3Widget w;
logAndProcess(w); // call with lvalue
logAndProcess(std::move(w)); // call with rvalue
在logAndProcess中,参数param被传递给函数process。当我们使用左值调用logAndProcess时,我们自然希望将左值作为左值转发到process,当我们用rvalue调用logAndProcess时,我们期望调用process的rvalue重载版本。
但是,与所有函数参数一样,param是一个左值。因此,对logAndProcess内的每个process调用都将调用左值重载版本。 为了防止这种情况,我们需要一种机制来将param强制转换为rvalue。这解释了为何forward是一种条件转换:只有当它的参数用rvalue初始化时才转换为rvalue。
你可能想知道std::forward是如何知道它的参数是否用rvalue初始化。简单地来说,该信息在logAndProcess的模板参数T中被编码,该参数传递给std::forward,后者恢复编码信息,具体可见Item28。
move与forward
鉴于std::move和std::forward的本质均为强制转换,唯一的区别在于std::move总是执行强制转换,而std::forward则为条件式转换,似乎总可以用forward代替move,从技术层来说确实如此,但却并无必要,毕竟真要取消的话不如干脆都写cast好了。
std::move的优势在于便捷,减少错误发生的可能性以及使代码更加清晰。假设当前我们需要跟踪移动构造函数的调用次数,那我们只需要增加一个static计数器。更进一步地,假设类中唯一的非静态数据是std::string,以std::move实现移动构造有如下实现:1
2
3
4
5
6
7class Widget {
public:
Widget(Widget&& rhs):s(std::move(rhs.s)){ ++moveCtorCalls; }
private:
static std::size_t moveCtorCalls;
std::string s;
};
如果以forward替换move,则代码将如下所示:1
2
3
4
5class Widget {
public:
Widget(Widget&& rhs):s(std::forward<std::string>(rhs.s)){ ++moveCtorCalls;}
…
};
可以看出,std::move只需要一个函数参数(rhs.s),而std::forward需要一个函数参数(rhs.s)和一个模板类型参数(std::string)。此外,我们传递给std::forward的类型应当是non-reference,因为这是编码的约定(见Item28)。总之,这些意味着move比forward在某些场合更加好用。
总而言之,std::move表示将参数无条件转为rvalue,而std::forward仅表示在传递参数时保持参数原有的左右值属性,此二者完全不同,因此需要两种不同的名称加以区分。
总结
- std::move表示将参数无条件转为rvalue,并不执行任何移动操作。
- std::forward将绑定至右值的参数转为右值。
- move与forward均不在运行期执行任何操作。