使字符串在std::move()之后为空的机制

mechanism that make a string empty after std::move() it

本文关键字:之后 机制 字符串 std move      更新时间:2023-10-16

我对std::move()如何真正清空某些东西感到困惑。

我写了一些代码:

int main()
{
    string str1("this is a string");
    std::cout<<std::boolalpha<<str1.empty()<<std::endl;
    string str2(std::move(str1));
    cout<<"str1: "<<str1.empty()<<endl;
    cout<<"str2: "<<str2.empty()<<endl;
}

输出为:

错误

true//这意味着原始字符串被清空

错误

为什么每次都清空原始字符串?我读过一些关于移动语义的材料,包括它的原始提议(这一个),它说:

复制和移动的区别在于,复制会使源保持不变。另一方面,移动会使源处于为每种类型定义不同的状态。源的状态可能没有变化,或者它可能完全不同。唯一的要求是对象保持自一致状态(所有内部不变量仍然完好无损)。从客户端代码的角度来看,选择移动而不是复制意味着您不关心源代码的状态会发生什么。

因此,根据这个词,上面str1的原始内容应该是某种未定义的。但为什么每次move()都会清空呢?(事实上,我已经在std::stringstd::vector上测试了这种行为,但结果是相同的。)

为了了解更多信息,我定义了自己的字符串类进行测试,如下所示:

class mstring
{
private:
    char *arr;
    unsigned size;
public:
    mstring():arr(nullptr),size(0){}
    mstring(char *init):size(50)
    {
        arr = new char[size]();
        strncpy(arr,init,size);
        while(arr[size-1] != '') //simply copy 
        {
            char *tmp = arr;
            arr = new char[size+=50]();
            strncpy(arr,tmp,50);
            delete tmp;
            strncpy(arr-50,init+(size-50),50);
        }
    }
    bool empty(){ return size==0;}
}

做同样的事情:

int main()
{
    mstring str("a new string");
    std::cout<<std::boolalpha<<str.empty()<<std::endl;
    mstring anotherStr(std::move(str));
    std::cout<<"Original: "<<str.empty()<<std::endl;
    std::cout<<"Another: "<<anotherStr.empty()<<std::endl;
}

输出为:

错误

Original:flase//表示原始字符串仍在中

另一个:false

甚至我添加了一个类似于这样的移动构造函数:

    mstring(mstring&& rvalRef)
    {
        *this = rvalRef;
    }

结果仍然是一样的。我的问题是:为什么std::string被清空了,而我自己定义的却没有?

因为std::string移动构造函数就是这样实现的。它拥有旧字符串的内容(即动态分配的char数组)的所有权,使旧字符串一无所有。

另一方面,mstring类实际上并没有实现移动语义。它有一个move构造函数,但它所做的只是使用operator=复制字符串。更好的实施方式是:

mstring(mstring&& rvalRef): arr(rvalRef.arr), size(rvalRef.size)
{
  rvalRef.arr = nullptr;
  rvalRef.size = 0;
}

这将内容传输到新字符串,并使旧字符串处于默认构造函数创建它的相同状态。这避免了分配另一个数组并将旧数组复制到其中的需要;相反,现有数组只得到一个新的所有者。

因此,根据这个词,上面str1的原始内容应该是某种未定义的

关于状态应该是什么,绝对没有任何东西。规范说明了状态可以是什么:任何都定义得足够好,可以销毁或分配新值。

Empty符合任何条件,因此状态可以为空。

在这种情况下,这也是最有意义的。字符串本质上类似于

class string {
    char *_M_data;
    size_t _M_size;
    size_t _M_alloc;
public:
    ...
}

其中_M_data是用new分配的,并且必须用delete删除(这可以用分配器参数自定义,但默认分配器只是这样做)。

现在,如果您不关心源的状态,那么最快的方法就是将缓冲区分配给目标,并用源中的nullptr替换缓冲区(这样它就不会被删除两次)。没有缓冲区的字符串为空。