【问题标题】:What happens to an object instance after applying std::move应用 std::move 后对象实例会发生什么
【发布时间】:2015-09-06 22:29:34
【问题描述】:

我正在尝试了解 std::move 和 rvalues 在 C++ 11 中的工作原理。我在这样的教程中不断看到类似的示例:

假设我们有这个类:

class Class 
{
   public:
      Class(Class &&b)
      {
          a = b.a;
      }
      int *a;
}

int main()
{
    Class object1(5);
    Class object2(std::move(object1));
}

main 函数的第二行运行后,object1 会发生什么? 如果 object1 的内存被“移动”到 object2,那么这个复制构造函数的意义何在?因为我们正在丢失 object1 的内存只是为了在内存的不同位置获得完全相同的值?这个有什么用处?

编辑:问题不是重复的。重复的候选者更广泛,因为它甚至没有代码 sn-p。

Edit2:我刚刚尝试了这段代码:

class trial
{
public:
    trial()
    {
        a = (int*)malloc(4);
    }

    trial(trial& rv)
    {
        this->a = rv.a;
        rv.a = NULL;
    }

    int *a;
};

int main() {
    cout << "Program Started" << endl;
    trial a1;
    trial a2(a1);


    return 0;
}

我已经检查了 a1 和 a2 的内部结构。它给出了与此代码完全相同的结果:

class trial
{
public:
    trial()
    {
        a = (int*)malloc(4);
    }

    trial(trial&& rv)
    {
        this->a = rv.a;
        rv.a = NULL;
    }

    int *a;
};

int main() {
    cout << "Program Started" << endl;
    trial a1;
    trial a2(std::move(a1));


    return 0;
}

不同之处在于复制构造函数,其中之一不使用移动语义。只是对象的简单引用。如果我们也通过引用传递,则不会发生复制,但我们将不得不通过执行rv.a = NULL 来稍微丢失第一个对象,以避免通过释放a1 意外释放a2 的内存。所以我设置了rv.a = NULL

当我使用类试验的右值复制构造函数,但不使用右值构造函数中的行rv.a = NULL 时,整数指针aa1a2 中显示相同的地址,当我放一个return 0 行的断点。那么这与仅通过引用传递有什么不同呢?看起来我们可以通过引用传递来做同样的事情。

【问题讨论】:

标签: c++ std move-semantics


【解决方案1】:

什么都没有。

std::move 不动。它只是将对象转换(转换)为一个右值引用,这可以通过查看一个典型的实现来看出:

template <typename T>
typename remove_reference<T>::type&& move(T&& arg)
{
  return static_cast<typename remove_reference<T>::type&&>(arg);
}

请注意,T&amp;&amp; arg 是可演绎上下文中的通用引用,而不是右值引用本身(如果您想知道“arg 不是右值引用吗?”)

使用右值引用的函数,如移动构造函数和移动赋值运算符或带有&amp;&amp; args 的常规函数​​,可以利用这个值类别(称为xvalue 即过期对象)并通过移动来避免开销从对象中取出数据,使其处于有效但未指定的状态(例如可破坏)。

根据 EDIT 2

我想你回答了你自己的问题。 假设你在类中有构造函数,移动和复制std::move 所做的就是让你在调用时选择第一个

trial a2(std::move(a1));

由于您对两者的实现是相同的,它们将做同样的事情。一个典型的实现会避免复制构造函数中的别名:

trial(trial& rv)
{
    this->a = (int*)malloc(sizeof(int));
    this->a = rv.a;
}

这意味着需要执行额外的分配(您只是想要一个副本,为什么要弄乱原始文件?)。

另一方面,当调用移动构造函数时,你基本上是在告诉编译器“嘿,我不再使用 a1,尽力而为”,然后调用移动构造并“移植”@987654331 @资源到a2

【讨论】:

  • 对不起。我无法完全理解这个例子。原谅我的经验不足。 remove_reference::type 到底是什么?
  • 只是添加到这个。像向量这样的对象分配内部存储,因此当基于过期对象创建对象时,无需复制内部存储,只需转移内部存储的所有权即可。
  • 谢谢多伦。我实际上觉得这是 std::move 和右值引用的主要用例。因为很自然,通常情况下,当我们填充向量之类的东西时,我们只需要向量,而不是对象本身。
  • @OzumSafa 你应该看看type_traits。为简化起见,代码是对编译器说的一种精心设计的方式:“我知道一些你不知道的事情;这个对象实例在函数之后就已经过期了,所以如果有的话,调用一个利用这个的覆盖"
  • 我也不明白你说T&amp;&amp; arg 是通用参考的部分。有没有办法可以详细说明或者我应该自己挖掘?
【解决方案2】:

应用 std::move 后对象实例会发生什么?"

什么都没有。之后它将被视为任何其他对象。这意味着仍将调用析构函数。正如 rems4e 已经提到的,您应该转移状态(例如,通过复制指针,因为这很便宜)并且保留原始对象不引用它以前的资源(如果析构函数试图释放它们应该)或其他一些定义的状态。

"main函数第二行运行后,object1会发生什么?"

您点击了范围退出},这会触发析构函数调用。首先是object2,然后是object1

如果object1 的内存被“移动”到object2,这个复制构造函数的意义何在?因为我们正在失去对object1 的记忆,只是为了在内存的不同位置获得完全相同的值?这个有什么用处?

将其视为专业。虽然真正的复制构造函数使您能够复制一个对象(深入到它的叶子,例如,在将object1 分配给object2 时)这可能非常非常昂贵,但移动构造函数使您能够快速转移状态只需复制其成员的指针。这在从函数返回时会派上用场。

这是一个例子:

#include <iostream>
#include <memory>
#include <string>

using namespace std;

class Person {
private:
    shared_ptr<string> name;
public:
    Person(shared_ptr<string> name) {
        cout << "Constructing " << *name << endl;
        this->name = name;
    }
    Person(const Person& original) {
        cout << "Copying " << *original.name << endl;
        name = make_shared<string>("Copy of " + *original.name);
    }
    Person(Person&& original) {
        cout << "Moving " << *original.name << endl;
        name = make_shared<string>(*original.name + ", the moved one");
        original.name = make_shared<string>("nobody (was " + *original.name + ")");
    }
    ~Person() {
        cout << "Destroying " << *name << endl;
        name = make_shared<string>();
    }
};

Person give_it_here(shared_ptr<string> name) {
    return Person(name);
}

int main(int argc, char* argv[]) {
    {
        Person p1(make_shared<string>("John"));
        Person p2 = move(p1); // Unnecessarily moving to another variable. It makes no sense.
    }
    cout << endl;

    {
        Person p1(make_shared<string>("James"));
        Person p2 = p1; // Copying here. Could make sense, but it depends.
    }
    cout << endl;

    {
        Person p1 = give_it_here(make_shared<string>("Jack")); // Let some other function create the object and return (move) it to us.
    }

    return 0;
}

代码打印(使用带有 C++11 和 -fno-elide-constructors 的 g++)

建造约翰 移动约翰 摧毁被感动的约翰 摧毁任何人(是约翰) 构建詹姆斯 抄袭詹姆斯 销毁詹姆斯的副本 摧毁詹姆斯 建造杰克 移动杰克 摧毁任何人(是杰克) 搬家杰克,搬家的人 摧毁任何人(是杰克,被感动的人) 摧毁杰克,被感动的人,被感动的人

备注:

  • 需要该标志 -fno-elide-constructors 以防止返回值优化(对于本示例)
  • 出于某种我无法理解的原因,g++ 生成了两个动作,而不是上一个示例中的一个动作

【讨论】:

  • @HanLuo,代码可以编译。我提供的输出是一个副本。确保启用了正确的 C++ 环境(使用 C++ 11 测试)。另外:简单地声称代码无法编译并不是很有建设性。什么是编译错误?如果您无法确定正确的项目设置,请考虑在 Stack Overflow 上提问。这就是这个网站的目的。
  • 这里没有冒犯。您只需将代码复制到cpp.sh。代码不会使用“C++11 Many (-Wall) Full (-O2)”设置编译。也可以直接查看这个onlinegdb.com/V6oPTd5CQ
  • @HanLuo,我查看了错误(感谢指出)并更正了它们。我忘记了我第一次使用的编译器和项目设置(除了 C++11)——也许是 MSVC,但它真的可以是任何东西。现在应该好多了。在我看来,新版本更简洁(不完美,从来都不是),因为它不再使用原始指针。
【解决方案3】:

典型的用例是避免复制大型资源(例如成员std::vector)。如果没有移动语义,它就会被复制。有了它,移动基本上是指针交换,可以快很多。

请注意,已移动到另一个对象的对象必须仍处于有效状态,因为它的析构函数仍将被调用。 因此,在您的示例中,您最好将a 成员设置为nullptr,因为它不能再拥有该资源(以避免双重删除)。

【讨论】:

  • 如果编写移动构造函数,这是你的责任;对于像 std::vector 这样的标准类,库已经为您完成了。它只是需要生存的析构函数,仅此而已;调用任何其他方法都是未定义的行为。
  • 他写了move构造函数;这就是为什么我的第二段在这里。我删除了最后一句的一部分以反映您对 UB 的引用。
【解决方案4】:

std::move 将通过将左值对象的类型转换为右值引用来实现资源的所有权转移,包括从 object1 到 object2 的内存。因此,您的移动复制构造函数或移动分配运算符可以启动资源传输。那么 obj1 的资源现在是 object2 的资源,如果 object1 是可移动的,那么在你的情况下它是。

【讨论】:

    【解决方案5】:

    std::move 根据提供的对象返回一个右值引用。这个新创建的右值引用现在可以传递给将右值引用作为参数的函数,例如void foo (Obj&amp;&amp; rvalue_reference)

    在不详细说明什么是右值引用的情况下,您真正​​需要知道的是右值预计会立即过期并且不会再被使用。这允许库编写者做出某些优化假设,否则他们将无法做出这些假设。

    例如:

    让我们采用如下函数:

    std::string addFullStopToEnd(std::string& str)
    

    在上面的函数中,我们必须创建一个全新的字符串来返回,因为我们快乐的用户可能仍然想要使用原始字符串。

    在函数中:

    std::string addFullStopToEnd(std::string&& str)
    

    我们可以将str 的内部存储附加句号并返回。这是安全的,因为右值引用是临时对象,因此无需保留它们以供以后使用。

    因此,从实际的角度来看,如果使用 std::move 修改函数参数,则声明一旦从函数返回,将永远不会引用该参数。这样做会导致未定义的行为。

    有关右值引用的更多信息,请查看here

    【讨论】:

    • 我熟悉右值的概念。所以你试图逃避复制和创建一个新对象。我很好奇旧对象会发生什么,也就是 rvalue str 资源的所有者。它处于什么样的状态?
    • 我认为对旧对象做出任何假设都不安全。或许一些标准大师可以给你一个更明确的答案。
    【解决方案6】:

    请注意移动构造函数的定义,对于以下示例

    #include <iostream>
    #include <vector>
    using namespace std;
    
    class A{
    
    public:
        int *ptr;
    
      A(int x){
        // Default constructor
        cout << "Calling Default constructor\n";
        ptr = new int ;
        *ptr = x;
      }
    
      A( const A & obj){
        // Copy Constructor
        // copy of object is created
        this->ptr = new int;
        // Deep copying
        cout << "Calling Copy constructor\n";
      }
    
      A ( A && obj){
        // Move constructor
        // It will simply shift the resources,
        // without creating a copy.
         cout << "Calling Move constructor\n";
        this->ptr = obj.ptr;
        obj.ptr = NULL;
      }
    
      ~A(){
        // Destructor
        cout << "Calling Destructor\n";
        delete ptr;
      }
    
    };
    
    int main() {
    
      A b = A(2);
      {
          vector <A> vec;
    
      vec.push_back(std::move(b));
      }
     cout << *(b.ptr);  //Segmentation fault (core dumped)
      return 0;
    
    }
    

    出现segmentation fault是因为b.ptr的内存在vector vec的作用域之后被释放。

    【讨论】:

      猜你喜欢
      • 2014-12-31
      • 1970-01-01
      • 2019-04-23
      • 2020-11-16
      • 2016-11-23
      • 2019-07-09
      • 2020-10-18
      • 2016-07-19
      • 2014-01-17
      相关资源
      最近更新 更多