std::move是否适用于左值引用?std::move如何在标准容器上工作?

14
#include <vector>

struct A { int a[100]; };

void foo (const A& a) {
  std::vector<A> vA; 
  vA.push_back(std::move(a));  // how does move really happen?
}

int main () {
  A a;
  foo(a);
}

以上代码编译正常。现在到处都写着move可以避免复制。
以下是我的问题:

  1. move在处理左值[非]const引用时是否真的有效?
  2. 即使使用“右值引用”,当对象插入类似上面的标准容器时,如何避免复制?

例如

void foo (A&& a) {  // suppose we invoke this version
  std::vector<A> vA; 
  vA.push_back(std::move(a));  // how copy is avoided?
}

1
它的工作原理是std::move(a)产生一个const A &&(请记住,std::move仅将其参数转换为右值引用)。但是,并不存在push_back(const A &&)--只有push_back(const A &)push_back(A &&)--,并且将调用第一个,导致发生复制(因为您无法将const右值引用绑定到非const右值引用,但可以将其绑定到const左值引用)。 - peppe
你说的“如何避免拷贝”是什么意思?你是在问如何实现vector::push_back(T &&)吗? - Steve Jessop
@SteveJessop,从某种程度上来说是的。但我不想深入细节。我的基本问题是,由于A&& a可能来自外部并在某个位置创建,而std::vector是连续容器,如何完全避免复制? - iammilind
3
如果值类型 T 是来自您的代码中的 struct A,那么移动该类型与复制它完全相同,因此并没有避免复制。对于具有有用的移动构造函数的类型(例如 vector 本身),它可以执行比复制构造函数更快的操作,那么我们就可以使用它。但是,如果一个类型将其所有数据存储在对象本身内部(即没有像 vector 那样的外部数据结构),那么一般而言,您无法编写有用的移动构造函数/赋值运算符。 - Steve Jessop
4
需要记住的是,可复制(Copyable)的概念是可移动(Movable)概念的一个子集。凡是可复制的对象 自动 也是可移动的,即使 C++ 中所称的移动实际上只是一种复制。这就是为什么移动操作不会指定源对象的状态:其允许源对象的状态保持不变。然而,并不是所有可移动的对象都是可复制的(例如 unique_ptr),而对于某些可复制的对象来说,移动和复制是不同的(例如 vector)。 - Steve Jessop
(我在术语上有些宽松:实际上,MoveConstructible和MoveAssignable是单独的概念,而不是单个可移动的概念,但与复制的关系对于每个概念都是相同的) - Steve Jessop
2个回答

18

std::move并不会移动对象,它实际上将左值引用强制转换为右值引用。在这种情况下,移动的结果是一个const A &&(顺便说一下,这完全没有用处)。

std::vector有一个重载形式,可以接受const A &A &&,所以会选择带有const A &的重载形式,并且const A &&会被隐式转换为const A &

std::move能够作用于const对象,对大多数程序员而言,这是奇怪/意外的行为,尽管它确实被允许。(很可能他们有使用它的用例或者没有防止它的理由)

对于您的示例更具体地说,将调用类A的移动构造函数。由于A是一个POD类型,这很可能只会进行复制,因为所有位都只需要移动/复制到A的新实例中。

由于标准仅规定原始对象必须处于有效但未指定的状态,因此编译器可以使A中的位保持不变,而不必将它们全部重置为0。实际上,大多数编译器都会保持这些位不变,因为更改它们需要额外的指令,这对性能很不利。


为什么会这样奇怪?在C++11中,const T&可以绑定到右值的事实并不是新鲜的。 - user743382
8
当你考虑到在C++中,将std::move添加到一些代码中,否则会导致复制,以及通常的可移动概念意味着“如果可能,则移动,否则复制”,那么这就不再奇怪了。它们并不意味着“如果可能,则移动,否则编译失败”。 - Steve Jessop
1
好的,那么为什么还会感到奇怪呢?在C++11中,const限定的右值并不是新的概念。 :) - user743382
1
你的两个 foo 函数将会表现得完全相同,因为你没有定义移动构造函数(也没有任何可移动资源的 A 类)。 - Miles Budnek
1
在这种情况下,移动构造函数是隐式定义的,虽然最终你将不得不复制 A::a,因为数组是不可移动的,但第一个版本的 foo 将调用 push_back(A const&),而第二个版本将调用 push_back(A&&),所以这些函数的行为并不完全相同。 - Holt
显示剩余5条评论

3
创建了一个代码片段来展示它。尽管在你的例子中会调用默认构造函数,但你已经理解了这个想法。
#include <vector>
#include <iostream>

struct A { 
  int a[100];
  A() {}
  A(const A& other) {
    std::cout << "copy" << std::endl;
  }
  A(A&& other) {
    std::cout << "move" << std::endl;
  }
};

void foo(const A& a) {
  std::vector<A> vA; 
  vA.push_back(std::move(a));
}

void bar(A&& a) {
  std::vector<A> vA; 
  vA.push_back(std::move(a));
}

int main () {
  A a;
  foo(a);            // "copy"
  bar(std::move(a)); // "move"
}

2
如果你写成 foo(std::move(a)); 会更加具有示范意义。 - Slav

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接