23 Understand std::move and std::forward
从 std::move
和 std::forward
什么都不做的方面学习,有助于理解其本质。std::move
什么也不移动。std::forward
什么也不转发。它们不会编译成任何可执行代码。
std::move
和 std::forward
是转换(cast
)函数。std::move
无条件将参数转成右值。std::forward
仅在条件满足的情况下做转换。这个解释可能带来新的问题,但是这就是这两个函数的全部内容了。
下面详细解释 std::move
。下面是 C++11 中 std::move
的实现,与标准库不同,但是很接近。
template <typename T> // in namespace std
typename remove_reference<T>::type &&
move(T &¶m)
{
using ReturnType = typename remove_reference<T>::type &&;
return static_cast<ReturnType>(param);
}
move
。其次,最核心的就是类型转换。给定一个对象的引用(准确的说是通用引用,详见 Item 24),返回的也是该对象的引用。
函数返回类型中 &&
表示 std::move
返回右值引用,但是如 Item 28 所说,如果 T
是左值引用,那么 T&&
是左值引用。为了正确性,我们使用 type traits(Item 9)std::remove_reference
确保 &&
应用于非引用类型。std::move
最后返回右值引用,从函数中返回右值引用,那么结果类型是右值。这就是 std::move
所做的全部了:将参数转换成右值。
C++14 可以简化实现
template <typename T> // C++14; still in
decltype(auto) move(T &¶m) // namespace std
{
using ReturnType = remove_reference_t<T> &&;
return static_cast<ReturnType>(param);
}
因为 std::move
只是将类型转化成右值,所以有人提议将名字改为 rvalue_cast
或者类似的东西。但是这都不重要哦,重要的是现在它的名字就是 std::move
,记住其所作的事情就好。
右值是移动操作的候选,所以 std::move
就是告诉编译器这个对象更适合移动。所以叫 std::move
。
但是,右值往往是移动操作的候选。假定有一个类,表示一段注解,其构造函数接受 std::string
类型参数,然后复制到类成员变量。假定使用 Item 41 的建议,使用值传递的方式。
class Annotation
{
public:
// param to be copied,so per Item 41, pass by value
explicit Annotation(std::string text);
};
text
的内容而不需要修改,根据传统的 const
应加尽加的原则,函数签名如下
为了避免一次拷贝成本,根据 Item 41 的建议,使用 std::move
class Annotation
{
public:
explicit Annotation(const std::string text)
: value(std::move(text)) // "move" text into value; this code
{ // doesn't do what it seems to!
}
private:
std::string value;
};
text
是拷贝到 value
中的,而不是移动。text
被 std::move
转换成了右值,但是一开始 text
的类型是 const std::string
,结果类型是 const std::string
的右值。const
属性被保留了下来。
考虑一下当编译器必须确定要调用哪个 std::string
构造函数时所产生的影响。
class string
{ // std::string is actually a typedef for std::basic_string<char>
public:
string(const string &rhs); // copy ctor
string(string &&rhs); // move ctor
};
在 Annotation
构造函数中,调用 std::string
的构造函数参数是 std::move(text)
,是 const std::string
的右值。这无法使用 std::string
的移动构造函数,因为后者的参数是非 const
的 std::string
的右值引用。但是这个右值可以传递给拷贝构造函数,因为 const
的左值引用允许绑定一个 const
右值作为参数。即使 text
已经被转换成了右值,但是调用的仍旧是拷贝构造函数。这是为了符合 const
语义,因为移动某个对象往往意味着修改这个对象,而语言不允许传递 const
对象给一个可能修改它们的函数。
当希望能够移动对象的时候,不要声明为 const
,因为移动 const
对象会变成拷贝操作。std::move
没有移动任何东西,也不保证它转换后的对象一定被移动。唯一能确定的事情是得到一个右值。
std::forward
与 std::move
类似,不过后者是是无条件转换,而前者只在条件满足的情况下转换。我们看一个 std::forward
的典型场景,一个模板函数,接受通用引用作为参数,然后转发给另一个函数。
void process(const Widget &lvalArg); // process lvalues
void process(Widget &&rvalArg); // process rvalues
template <typename T> // template that passes
void logAndProcess(T &¶m) // param to process
{
// get current time
auto now = std::chrono::system_clock::now();
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param));
}
logAndProcess
函数内部,参数 param
传递给了函数 process
。process
重载了两个实现,一个参数是左值,一个参数是右值。当调用 logAndProcess
时,如果传递了左值,我们期望 process
的左值版本被调用;如果传递了右值,很明显,希望 process
的右值版本被调用。
与所有函数的参数一样,param
是左值,那么 logAndProcess
总是调用 process
的左值版本。为了阻止此事,当传入 logAndProcess
的参数 param
是右值时,std::forward
将 param
转换成右值,调用 process
的右值版本。所以说 std::forward
是有条件转换:当参数是右值时,将其转换成右值。
std::forward
是如何知道参数是左值还是右值的呢?也就是它怎么判断 param
是左值还是右值的?简单的回答,logAndProcess
函数的模板参数 T
包含了这些信息。详见 Item 28。
这两个函数都可以归纳为转换,那么是不是只是用 std::forward
呢?从技术角度看,回答是肯定的。std::forward
可以胜任所有的工作,而无需使用 std::move
。这两个函数都不是必须的,我们可以自己手写转换,但这相当恶心。
std::move
的好处是方便、不易出错、更清晰。假定我们需要统计移动构造函数调用的次数,我们只需要添加一个 static
变量,然后在移动构造函数中自增。常见的实现如下。
class Widget
{
public:
Widget(Widget &&rhs)
: s(std::move(rhs.s))
{
++moveCtorCalls;
}
private:
static std::size_t moveCtorCalls;
std::string s;
};
std::forward
的版本如下
class Widget
{
public:
Widget(Widget &&rhs) // unconventional,
: s(std::forward<std::string>(rhs.s)) // undesirable implementation
{
++moveCtorCalls;
}
};
首先,std::move
只需要一个函数参数 rhs.s
,而 std::forward
除了函数参数 rhs.s
之外,还需要模板类型参数 std::string
。其次,我们传递给 std::forward
的参数应该是一个右值(详见 Item 28),是非引用类型。这意味 1)使用 std::move
少打字,不用传递右值参数的类型参数。2)减少传了不正确类型的风险,比如写成了 std::string&
就会导致初始化 s
使用拷贝构造而不是移动构造。
更重要的是,使用 std::move
意味着无条件转换为右值,而使用 std::forward
意味着只对绑定了右值的引用转换成右值,这完全是不同的操作。如它们应用场景那样,前者为了移动场景,而后者为了转发参数。正因如此,使用不同的函数(函数名)以示区别。
Things to Remember
std::move
performs an unconditional cast to an rvalue. In and of itself, it doesn't move anything.std::forward
casts its argument to an rvalue only if that argument is bound to an rvalue.- Neither
std::move
norstd::forward
do anything at runtime.