C++移动语义和内置类型

4
我正在尝试使用以下代码理解移动语义。
#include <iostream>
#include <utility>
using namespace std;

class A 
{
   public:
   int a;
   A():a{1}{}
   A(A&& rref):a{rref.a}{cout<<"move constructor called"<<endl;}
   A(const A& ref):a{ref.a}{cout<<"copy constructor"<<endl;}
};

int main(){

  A original; // original object

  cout<<"original.a = "<<original.a<< "|  address original.a="<< &(original.a)<<endl;

  A movedto (std::move(original)); // calls A(A&&)

  cout<<"original.a = "<<original.a<< "| address original.a"<< &(original.a)<<endl;
  cout<<"movedto.a = "<<movedto.a<<"| address movedto.a"<< &(movedto.a)<<endl;

  return 0;
}

以下是输出结果。
original.a = 1|  address original.a=0x7fff1611b6d0
move constructor called
original.a = 1| address original.a0x7fff1611b6d0
movedto.a = 1| address movedto.a0x7fff1611b6e0

如您所见,original.amovedto.a的地址不同,因此成员变量aA(A&& rref):a{rref.a}中进行了复制操作。

我知道对于内置类型,移动操作会退化为复制操作。我的问题是,如果我想劫持(而非复制)类的实例该怎么办?假设我有100个内置类型的成员变量(而非只有一个),那么复制的代价就很大了。

一种明显的方法是将对象存储在堆上,并使用引用语义来传递它。但我想保持值语义,并仍然能够规避复制。


2
复制100个整数仍然不算昂贵。但是你可以使用一个std::vector<int>成员并移动它。它会为您处理一切。 - Bo Persson
1
两个问题:1)std::vector<T>是否具有值语义?2)如果问题1的答案是肯定的,那么vector<T>会做什么? - Howard Hinnant
1
a{rref.a} 应该改为 a{std::move(rref.a)} 以实际移动 a(尽管对于 int 来说仍然会复制,因为它是相同的)。 - NathanOliver
2
你在这里使用内置类型是无关紧要的。无论你将a定义为什么类型,original.amovedto.a仍然具有不同的地址。 - Benjamin Lindley
1
不,@NathanOliver是正确的,rref.a是一个lvalue,因为rref是一个lvalue(命名的右值引用)。如果你想让它成为xvalue,你需要std::move(rref.a)。或者,如果你做了std::move(rref).a,那么它也可以工作,因为这样运算符将从一个rvalue提供一个rvalue。 - wally
显示剩余6条评论
1个回答

1

这是不可能的,和/或者没有意义。


我们开始:

A original; // original object

问题的主要部分是:

如果我想劫持(而不是复制)类的一个实例,我该怎么做?

这意味着我们最终会得到:

A movedto; // new object that has all of original's members

但这里有一个警告,我们想要“规避复制”,并且不想使用引用或指针,即“引用语义”,只使用“堆栈”或“值语义”。
如果我们希望movedto具有与已分配的相同的成员和内存位置,则可以创建对original的引用:
A& movedto{original}; // references members at the same memory locations. 

但是问题的一部分说明我们不使用引用,因为我们想让这个对象具有不同的生命周期。所以如果我们想要保留“original”的成员并使它们在当前块结束后继续存在,则立即发现我们无法控制底层内存。
在这个问题中,original是一个具有自动存储期的对象。具有自动存储期的对象的生命周期会根据它们的作用域自动管理。编译器可能使用堆栈来存储它,并且编译器可能使用栈指针,在添加对象时将其向下移动,但C++标准没有指定应该如何完成。我们知道,标准规定具有自动存储期的对象将在作用域结束时按创建顺序相反的顺序销毁。

因此,试图控制具有自动存储期的对象的创建位置是没有意义的,将这样一个对象的成员赋值给另一个也是没有意义的。内存是自动分配的。

如果我们想要重复使用已分配为具有自动存储期的对象的一部分的变量(堆栈/值语义),则我们正在使用将在该对象的生命周期结束时取消分配的内存。我们必须使用动态存储(即“堆”,或“引用语义”)。


两种方法都使用堆来存储实际数据,因此从顶部看起来像值语义,但在内部我们交换指针以实现移动。这让我相信以下内容。如果我错了,请纠正我。
  1. 数据应始终驻留在堆上,并具有管理句柄对象(unique_ptr或Impl*),我们在其上实现移动构造函数和赋值。
- Zanylytical Scientist
@Dhruv 是的,它们都使用动态存储期(堆存储)。而且,我没有看到在为具有自动存储期(栈)的对象分配的内存中执行此操作的方法。我已更新答案,重点关注问题的主要要点。 - wally
明白了。我们已经偏离了问题的主题,但我认为这就是我在寻找的东西。谢谢。 - Zanylytical Scientist

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