C++ 移动具有整数成员的对象的语义

C++ Move semantics with object having integer member

#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,但旧值仍然存在。

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

移动在C++中就是其他语言中所谓的浅拷贝

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

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

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

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

通常,移出的对象可以具有对其类型有效的任何值。例如,一个 moved-from std::string 可能会变成空的,也可能是完全不同的东西。它有时可能是空的,有时不是。可以是什么都没有限制,不应该依赖确切的值。

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

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

隐式 Move 构造函数对于非 class 类型的成员无法正常工作 但是如果你使用显式移动构造函数,那么你可以使用非class类型的交换函数。

#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("Whosebug ", 12);
    a.printInfo();
    A b = std::move(a);
    b.printInfo();
    a.printInfo();
    return 0;
}

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

如果您通常需要将内置类型设置为 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;
    }
};

我通常不喜欢这样做,因为在 class 中添加新成员时容易出错。他们需要添加到样板代码中。相反,您可以使用一个小助手 class,在使用默认移动语义移动时将特定成员设置为 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刚到:

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

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