移动语义和完美转发
要理解移动语义和完美转发,首先必须理解:
- 右值引用
- 万能引用和引用折叠
移动语义
移动语义和拷贝语义相对的,移动语义可以将资源从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁。
完美转发的实现主要依赖于两个关键要素:右值引用和移动构造函数/移动赋值操作符。
在转移资源后,被移动的对象处于“有效但未定义的状态”(valid but unspecified state)
struct Foo {
   Foo() { std::cout << "Constructed" << std::endl; }
   Foo(const Foo &) { std::cout << "Copy-constructed" << std::endl; }
   Foo(Foo &&) { std::cout << "Move-constructed" << std::endl; }
   ~Foo() {}
};
int main()
{
    // 默认构造函数
    Foo f1;
    // 拷贝构造函数
    Foo f2 = f1;
    // 调用移动构造函数:将左值f1强制转换为将亡值,
    // 调用移动构造函数构造一个新对象。这里发生了资源所有权的转移。
    Foo f3 = std::move(f2);
    // 没有调用构造函数:声明并初始化一个引用,
    // 而不是创建一个新对象,没有发生任何资源转移,只是延长了这个将亡值的生命周期。
    Foo&& f4 = std::move(f3);
}
output:
Constructed
Copy-constructed
Move-constructed
std::move
std::move是一个标准库函数,它的作用就是将一个左值强制转换为右值引用,除此之外,没有其他处理。
// 这里的move(T&& t)代表万能引用
template<typename T>
constexpr typename std::remove_reference<T>::type&& move(T&& t) noexcept
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}
std::move并不代表它真的移动了什么,它只是一个类型转换,告诉编译器这个对象可以被移动。
完美转发
完美转发(Perfect Forwarding)允许将函数模板中的参数完全转发给另一个函数,同时保持原始参数的值类别和常量性。
完美转发的实现主要依赖于两个关键要素:右值引用和std::forward函数模板。
std::forward
我们先来看std::forward的实现:
- typename std::remove_reference::type&将T的引用属性剥除,使得t只能是一个左值引用或者右值引用,从而可以精确匹配std::forward的2个重载版本。 
- std::forward的返回类型是T&&万能引用,这个返回类型能根据引用折叠规则,使得:
    - && + & -> &
- && + && -> &&
 
template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type& t) noexcept
{
    return static_cast<T&&>(t);  // 接收左值引用,返回万能引用
}
template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type&& t) noexcept
{
    return static_cast<T&&>(t);  // 接收右值引用,返回万能引用
}
举例说明:
void process(int& lval)  { /* 拷贝操作 */ }
void process(int&& rval) { /* 移动操作 */ }
template<typename T>
void wrapper(T&& arg) { // arg 是万能引用
    process(arg);       // 问题在这里
}
int main() {
    int x = 10;
    wrapper(x);         // 传入左值,arg 成为左值引用
    wrapper(20);        // 传入右值,arg 成为右值引用
}
上面例子中,当调用wrapper(20)时
- 20是一个右值, T被推导为`int,T&&被实例化为int&&, 所以arg是一个右值引用。
- int&&右值引用类型是一个具名变量,所以- arg被视为一个左值。这会导致- process的拷贝版本被调用,而不是移动版本。
这里就导致了问题: 给wrapper传入了一个右值,但是在wrapper函数内部,却调用了process的左值版本。
std::forward为了解决这个问题,它可以让:左值进来,左值出去;右值进来,右值出去。
std::forward会根据模板参数T的类型信息,并结合引用折叠规则,进行有条件的类型转换。
注意:
std::forward是利用模板参数T的类型,而不是arg的类型。
当在模板函数内部使用 std::forward<T>(arg) 时:
- 如果原始参数是左值:T被推导为int&。std::forward<int&>(arg)会返回int& &&,根据引用折叠,最终类型是int&,即左值引用。
- 如果原始参数是右值:T被推导为int。std::forward<int>(arg)会返回int &&,即右值引用。
std::forward 完美地保持了参数的原始属性。
void process(int& lval)  { /* 拷贝操作 */ }
void process(int&& rval) { /* 移动操作 */ }
template<typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg)); // 使用 std::forward 完美转发
}
int main() {
    int x = 10;
    wrapper(x);         // 传入左值,std::forward转发为左值引用,调用process(int&)
    wrapper(20);        // 传入右值,std::forward转发为右值引用,调用process(int&&)
}
们再来做一个总结:
- 第一个wrapper:
    - 当传入为x时,T被推导为int&,T&& 被推导为int&& &, 根据引用折叠,arg是一个左值引用,最终调用process的左值版本。
- 当传入为10时,T被推导为int,T&& 被推导为int &&,arg是一个右值引用,也就是一个左值,最终还是调用process的左值版本。
 
- 第二个wrapper:
    - 当传入为x时,T被推导为int&,编译器将std::forward实例化为std::forward<int&>(arg),std::forward返回值类型为T&&, 实例化后返回类型为int&& &, 根据引用折叠,最终返回类型为左值引用,调用process的左值版本。
- 当传入为10时,T被推导为int,编译器将std::forward实例化为std::forward<int>(arg),std::forward返回值类型为T&&, 实例化后返回类型为int&&,最终返回类型为右值引用,调用process的右值版本。
 
- 当传入为x时,T被推导为int&,编译器将