【问题标题】:C++ Move semantics with object having integer memberC++ 使用具有整数成员的对象移动语义
【发布时间】:2017-11-11 06:55:12
【问题描述】:
#include<iostream>
#include<stdio.h>

using namespace std;
class Test
{    
   public:
       string n;
       Test():n("test") {}  
};

int main()
{
    Test t1;  
    std::cout<<"before move"<<"\n";
    std::cout<<"t1.n=" << t1.n<<"\n";
    Test t2=std::move(t1);

    std::cout<<"after move"<<"\n";
    std::cout<<"t1.n="<<t1.n<<"\n";
    std::cout<<"t2.n="<<t2.n<<"\n"; 

    return 0;
}

上述程序的输出产生以下结果

搬家前 t1.n=测试 搬家后 t1.n= t2.n=测试

了解到,将对象 t1 移动到 t2 后,t2.n 的值结果为空字符串

但相同的概念移动概念不适用于整数。

#include<iostream>
#include<stdio.h>

using namespace std;

class Test
{

    public:
        int n;
        Test():n(5) {}  

};

int main()
{
    Test t1;  
     std::cout<<"before move"<<"\n";
     std::cout<<"t1.n=" << t1.n<<"\n";
     Test t2=std::move(t1);

     std::cout<<"after move"<<"\n";
     std::cout<<"t1.n="<<t1.n<<"\n";
     std::cout<<"t2.n="<<t2.n<<"\n"; 

     return 0;
}

上述程序的输出产生以下结果

搬家前 t1.n=5 搬家后 t1.n=5 t2.n=5

将对象 t1 移动到 t2 后,我预计 t2.n 的值为 0,但旧值仍然存在。

谁能解释一下这种行为背后的概念。

【问题讨论】:

  • std::string 不同,int 没有将其设置为空值的默认构造函数。这能回答你的问题吗?
  • @MrLister 具有默认构造函数在此处无效。从右值初始化基本类型(即移动)根本不会修改源对象。

标签: c++ c++11 constructor move semantics


【解决方案1】:

如果您通常需要将内置类型设置为 0,例如 int、float、指针,因为您可能在销毁时依赖它们,例如用户定义的 API 指针,您需要显式编写移动运算符将这些成员设置为零。

class MyClass
{
    std::string str_member_; // auto moved and emptied
    int a_value_;            // need to manually set to 0
    void* api_handle_;       // ''
public:

    // boilerplate code
    MyClass(MyClass&& rhd)
    {
        *this = std::move(rhd);
    }
    MyClass& operator=(MyClass&& rhd)
    {
        if (this == &rhd)
            return *this;

        str_member_ = std::move(rhd.str_member_);
        a_value_ = rhd.a_value_;
        api_handle_ = rhd.api_handle_;

        rhd.a_value_ = 0;
        rhd.api_handle_ = 0;

        return *this;
    }
};

我通常不喜欢这样,因为当新成员被添加到类中时它容易出错。他们需要被添加到样板代码中。相反,您可以使用一个小的帮助类,在使用默认移动语义移动时将特定成员设置为 0。

template<class T>
class ZeroOnMove
{
    T val_;
public:
    ZeroOnMove() = default;
    ZeroOnMove(ZeroOnMove&& val) = default;
    ZeroOnMove(const T& val) : val_(val) {}

    ZeroOnMove& operator=(ZeroOnMove&& rhd)
    {
        if (this == &rhd)
            return *this;
        val_ = rhd.val_;
        rhd.val_ = 0;
        return *this;
    }
    operator T() 
    {
        return  val_;
    }
};

在此之前的课程刚刚到达:

class MyClass
{
public:
    std::string str_member_;
    ZeroOnMove<int> a_value_;
    ZeroOnMove<void*> api_handle_;
};

也许这也有助于更多地理解移动语义。

【讨论】:

    【解决方案2】:

    隐式移动构造函数不适用于成员非类类型 但是如果你使用显式移动构造函数,那么你可以使用非类类型的交换函数。

    #include <utility>
    std::exchange(old_object, default_value) //explicit move of a member of non-class type
    

    下面是例子

    #include<iostream>
    #include<string>
    #include <utility>
    
    struct A
    {
        std::string name;
        int age;
        A(){
            std::cout << "Default ctor. ";
        }
    
        //explicit
        A(std::string const& s, int x):name(s), age(x){
            std::cout << "Ctor. ";
        }
    
        A(A const& a):name(a.name),age(a.age){
            std::cout << "Copy ctor. ";
        }
    
        A(A && a) noexcept :name(std::move(a.name)),age(std::exchange(a.age,0)){
            std::cout << "Move ctor. ";
        }
    
        A& operator=(A const& a){
            std::cout << "Copy assign. ";
            name = a.name;
            age = a.age;
            return *this;
        }
    
        A& operator=(A && a) noexcept {
            std::cout << "Move assign. ";
            name = std::move(a.name);
            age = std::move(a.age);
            return *this;
        }
    
        void printInfo()
        {
            std::cout<<name<<"   "<<age<<std::endl;
        }
    
        ~A() noexcept {
            std::cout << "Dtor. ";
        }
    };
    int main()
    {
        A a("StackOverflow ", 12);
        a.printInfo();
        A b = std::move(a);
        b.printInfo();
        a.printInfo();
        return 0;
    }
    

    了解更多信息 https://en.cppreference.com/w/cpp/language/move_constructor

    【讨论】:

      【解决方案3】:

      在 C++ 中的移动是在其他语言中称为浅拷贝

      复制:如果一个对象将数据存储在动态分配的内存中,那么(深度)复制意味着(1)分配一个等效的内存块和(2)将所有元素从一个块复制到其他。这会保留复制源对象并创建一个新的、完全独立的副本。

      move:如果同一个对象被移动,那么只有对象实际存储的数据(而不是动态分配内存中存储的数据)被复制过来(实际上是被移动了) ,即这是递归的),即保存内存块地址和大小信息的指针变量。同时,被移动的对象被“清空”,即进入(有效)状态,在销毁时不会影响被移动的对象。这意味着 必须将指向被移动对象的内存块的指针重置为nullptr,并且将引用内存大小的变量重置为零,您称为清空的过程。

      现在,对于不使用动态分配内存的对象,尤其是所有内置类型(例如int),移动和复制没有区别。特别是,将移动后的变量保留在其原始状态,即制作副本,这很好,并且实际上是标准要求的。 (标准可能未指定此值或要求重置为默认值,即 0,但事实并非如此。)

      另请参阅 here 以获得对移动语义的冗长描述。

      【讨论】:

      • The compiler could reset it to 0 (another valid state) 不,编译器不能。不修改右手边不仅是最明智的实现,也是标准要求的:if the subobject is of scalar type, the built-in assignment operator is used. 内置赋值运算符不修改右手边。
      • 感谢 Walter 的 cmets
      【解决方案4】:

      一般来说,移出对象可以具有对其类型有效的任何值。例如,从std::string 移出的地址可能为空,或者可能完全不同。它有时可能是空的,有时不是。它可以是什么没有限制,也不应该依赖确切的值。

      由于被移动的对象可以处于任何有效状态,我们可以看到复制对象是移动它的有效方式。事实上,对于任何没有定义移动构造函数的类型,在移动对象时都会使用复制构造函数。 std::move 不需要被移动的对象变为空(我们不一定要为所有类型定义的概念)。当然,复制可能不是移动对象的最有效方式,但它是允许的。基本类型基本上利用了这一点,所以移动一个基本类型就相当于复制它。

      我想再次强调这一点:不要(通常)依赖于移出对象的值。它通常不是一个指定的值。 不要假设移出的对象与默认构造的对象相同。 不要假定它是任何其他值。某些特定类型,例如int 或标准智能指针类型可能会指定移动的值,但这些是特殊情况,不定义一般规则。在将已知值复制到其中之前,不要使用已移动的对象通常是明智的。

      【讨论】:

      • any type which does not define a move constructor, the copy constructor will be used。我想详细说明一下,没有显式定义移动构造函数的类仍然可以隐式定义移动构造函数。
      • 感谢肯的解释。你是超级巨星!
      • 您关于使用移动对象的警告有点太强烈了。移出对象保证处于有效状态,即使用它不是未定义的。但是,除了内置类型之外,该状态是未指定的。
      • @user2079303 是的,这是真的。只要类没有用户声明的复制构造函数、复制赋值运算符、移动赋值运算符或析构函数,就会隐式定义移动构造函数。
      • @Walter 这不正是我所说的吗?
      猜你喜欢
      • 2012-01-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多