Item 23: Understand std::move and std::forward.

Item 23: Understand std::move and std::forward.,第1张

Item 23: Understand std::move and std::forward.

Effective Modern C++ Item 23 的学习和解读。


std::move 和 std::forward 并不像他们名字所表达的那样,实际上 std::move 并没有移动数据,std::forward 也并没有转发数据,并且它们在运行期什么也没做。


先说 std::move,我们看下它在 C++11 中简易的实现如下:

template<typename T>    // in namespace std
typename remove_reference<T>::type&&
move(T&& param)
{
  using ReturnType =                          // alias declaration;
	typename remove_reference<T>::type&&;  // see Item 9
  return static_cast<ReturnType>(param);
}

std::move 只是返回了右值引用。


这里使用了 remove_reference 是为了去除引用标识符。


当 T 是一个引用类型的时候,根据引用折叠原理,T&& 会被折叠成一个左值引用类型。


所以 remove_reference 是为了去防止 T 是一个引用类型, 它会去除引用进而保证 std::move 返回一个右值引用。


因此 std::move 只是做了类型转换,并没有移动数据。


由于只有右值是可以被移动的,std::move 更像是说明经过它之后对象可能会被移动(可能,而不是一定,后文会有解释)。


而 C++14 的 std::move 更加简洁:

template<typename T>             // C++14; still in
decltype(auto) move(T&& param)   // namespace std
{
  using ReturnType = remove_reference_t<T>&&;
  return static_cast<ReturnType>(param);
}

std::move 的目的就是让编译器把修饰的变量看做是右值,进而就可以调用其移动构造函数。


事实上,右值是仅可以被移动的对象,std::move 之后不一定一定调用构造函数。


看下面的例子,假如你有这样的一个类:

class Annotation {
  public:
    explicit Annotation(std::string text) : text_(text) 
    std::string text_;
}

class Annotation {
 public:
  explicit Annotation(std::string text) : text_(std::move(text)) {} 
  std::string text_;
}; 

class Annotation {
 public:
  //这里换成了带有const
  explicit Annotation(const std::string text) : text_(std::move(text)) {}
  std::string text_;
}; 

第一个实现会发生两次拷贝,第二个实现会发生一次拷贝和一次移动,那么第三个实现会发生什么呢?

由于 Annotation 的构造函数传入的是一个 const std::string text,std::move(text) 会返回一个常量右值引用,也就是 const 属性被保留了下来。


而 std::string 的 move 构造函数的参数只能是一个非 const 的右值引用,这里不能去调用 move 构造。


只能调用 copy 构造,因为 copy 构造函数的参数是一个 const 引用,它是可以指向一个 const 右值。


因此,第三个实现也是发生两次拷贝。


也可以用下面的例子验证一下:

#include 
#include 

using boost::typeindex::type_id_with_cvr;

class A {
public:
  A(){
    std::cout << "constructon" << std::endl;
  }
  A(const A& a) {
    std::cout << "copy constructon" << std::endl;
  }
  A(A&& a) {
    std::cout << "move constructon" << std::endl;
  }
};

int main() {
  const A a1;
  std::cout << type_id_with_cvr<decltype(std::move(a1))>().pretty_name() << std::endl;
  auto a2(std::move(a1));
    
  return 0;
}

// output
constructon
A const&&
copy constructon

因此,我们可以总结出两点启示:

  • 第一,假如你想对象能够真正被移动,不要声明将其申明为 const,对 const 对象的移动 *** 作会被转换成了拷贝 *** 作。


  • 第二,std::move 不仅不移动任何东西,甚至不能保证被转换的对象可以被移动。


    唯一可以确认的是应用 std::move 的对象结果是个右值。


再说 std::forward。


std::forward 也并没有转发数据,本质上只是做类型转换,与 std::move 不同的是,std::move 是将数据无条件的转换右值,而 std::forward 的转换是有条件的:当传入的是右值的时候将其转换为右值类型。


看一个 std::forward 的典型应用:

#include
#include

class Widget {
};

void process(const Widget& lvalArg) {
  std::cout << "process(const Widget& lvalArg)" << std::endl;
}

void process(Widget&& rvalArg) {
  std::cout << "process(Widget&& rvalArg)" << std::endl;
}

template<typename T>
void logAndProcess(T&& param) {
  auto now = std::chrono::system_clock::now();
  process(std::forward<T>(param));
}

int main () {
  Widget w;
  logAndProcess(w);              // call with lvalue
  logAndProcess(std::move(w));   // call with rvalue
}

// output
process(const Widget& lvalArg)
process(Widget&& rvalArg)

当我们通过左值去调用 logAndProcess 时,自然期望这个左值可以同样作为一个左值转移到 process 函数,当我们通过右值去调用 logAndProcess 时,我们期望这个右值可以同样作为一个右值转移到 process 函数。


但是,对于 logAndProcess 的参数 param,它是个左值(可以取地址)。


在 logAndProcess 内部只会调用左值的 process 函数。


为了避免这个问题,当且仅当传入的用来初始化 param 的实参是个右值,我们需要 std::forward 来把 param 转换成一个右值。


至于 std::forward 是如何知道它的参数是通过一个右值来初始化的,将会在 Item 28 中会解释这个问题。


总结一下:

  • std::move 无条件将输入转化为右值。


    它本身并不移动任何东西。


  • std::forward 把其参数转换为右值,仅仅在参数被绑定到一个右值时。


  • std::move 和 std::forward 只是做类型转换,在运行时(runtime)不做任何事。


欢迎分享,转载请注明来源:内存溢出

原文地址: https://www.outofmemory.cn/langs/562386.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-01
下一篇 2022-04-01

发表评论

登录后才能评论

评论列表(0条)

保存