std::move 更改变量地址?

std::move changes address of variables?

我发现了意外的(至少对我而言)行为。

class A
{
    char  _text[100];
    char* _beg;
    char* _end;
public:
    explicit A(char* text, size_t tsize) : _beg(&text[0]), _end(&text[std::min(tsize, 99)]) 
    {
        memcpy(_text, text, std::min(tsize, 99));
        *_end = '[=11=]';
    }
    inline std::string get_text()
    {
        return std::move(std::string(_beg, _end));  
    }
};

之后我在代码中的某处执行此操作:

  A* add_A(A&& a)
  {
     list_a.push_back(std::move(a));
     return &(list_a.back());
  }

  std::list<A> list_a;
  {
      add_A(A("my_text", 7));
      list_a.back().get_text(); //returns "my_text"
  }
  list_a.back().get_text(); //returns trash

因为只有我移动这个class(使用std::move),并调用被移动对象的get_text(),我得到垃圾,好像在变量[=的移动地址之后15=] 改变了,所以 _beg_end 指向任何地方。

std::move 之后变量的地址真的可以改变吗(我认为 move 并没有真正移动对象,它就是为此而发明的)?

如果可以更改,处理它的通常模式是什么(相应地更改指针)?

如果无法更改,可能会发生这种行为,因为我试图将此类对象移动到 std::list(因此不知何故发生了复制,它更改了变量的地址并使指针指向错误的位置)?

在 C++ 中移动只是一种特殊形式的复制,您可以在其中修改要移动的对象中的数据。这就是 unique_ptr 的工作原理;您将指针从一个 unique_ptr 对象复制到另一个对象,然后将原始值设置为 NULL。

当您移动一个对象时,您正在创建一个新对象,一个从另一个对象获取数据的对象。会员地址不要"change";只是不是同一个对象

因为你没有写一个copy/move构造函数,这意味着编译器会为你写一个。他们所做的就是复制每个元素。因此,新移动到的对象将有指向 旧对象的指针

即将被销毁的对象。

这就像搬进一栋恰好与旧房子一模一样的房子。无论它看起来多么像你的老房子,它都不是。你还是得改地址,因为是新房子。 _beg_end 的地址也必须更新。

现在,您可以创建一个移动 constructor/assignment 运算符(以及一个复制 constructor/assignment 运算符)来更新您的指针。但坦率地说,这只是对 糟糕设计 的掩盖。如果可以的话,在同一个对象中使用指向子对象的指针并不是一个好主意。而不是 begin/end 指针,只需要一个实际的 size:

class A
{
    char  _text[100];
    size_t _size;
public:
    explicit A(char* text, size_t tsize) : _size(tsize)
    {
        strncpy(_text, text, 100);
    }
    inline std::string get_text()
    {
        return std::string(_text, _size); //Explicit `move` call is unnecessary
    }
};

这样就不需要存储begin/end指针了。那些可以根据需要合成。

std::move 没有移动部分,它只是将输入参数提升为右值引用——请记住,在 foo(T&& t) { ... } 的主体内,按名称使用 t 的计算结果为左值(对右值的引用)。

inline std::string get_text()
{
    return std::move(std::string(_beg, _end));
}

分解:

std::string(_beg, _end);

创建一个从 _beg_end 构建的匿名临时 std::string 对象。这是一个右值。

std::move(...);

强制将其提升为右值引用并阻止编译器执行 return 值优化。你要的是

return std::string(_beg, _end);

assembly code comparison

您可能还想使用

list_a.emplace_back(std::move(a));

不幸的是,这种方法有两个缺陷。

更简单的是moving这个词可能有点误导,听起来很单调。但在实践中,它通常是双向交换:两个对象交换属性,以便当临时对象超出范围时,它会执行清理其他对象以前拥有的任何内容:

struct S {
    char* s_;
    S(const char* s) : s_(strdup(s)) {}
    ~S() { release(); }
    void release() { if (s_) free(s_); }
    S(const S& s) : s_(strdup(s.s_)) {}
    S(S&& s) : s_(s.s_) { s.s_ = nullptr; }
    S& operator=(const S& s) { release(); s_ = strdup(s); return *this; }
    S& operator=(S&& s) { std::swap(s_, s.s_); return *this; }
};

注意这一行:

    S& operator=(S&& s) { std::swap(s_, s.s_); return *this; }

当我们写:

S s1("hello");
s1 = S("world");

第二行调用移动赋值运算符。 hello 副本的指针移入临时文件,临时文件超出范围并被销毁,"hello" 副本被释放。

用你的字符数组进行这种交换的效率明显低于单向复制:

struct S {
    char s_[100];
    S(const S& s) {
        std::copy(std::begin(s.s_), std::end(s.s_), std::begin(s_));
    }
    S(S&& s) {
        char t_[100];
        std::copy(std::begin(s.s_), std::end(s.s_), std::begin(t_));
        std::copy(std::begin(s_), std::end(s_), std::begin(s.s_));
        std::copy(std::begin(t_), std::end(t_), std::end(s_));
    }
};

没有这样做,右值参数只需要处于安全销毁状态即可,但以上是默认的 移动操作员将要执行的操作。

你的代码的灾难性部分是默认的移动运算符是天真的。

struct S {
    char text_[100];
    char *beg_, *end_;
    S() : beg_(text_), end_(text_ + 100) {}
};

考虑以下复制构造:

S s(S());

s.beg_ 指向什么?

答:指向S().text_,不是s.text_。您需要编写一个复制构造函数来复制 text_ 的内容,然后将其自己的 beg_end_ 指向其自己的 text_ 而不是复制源值。

移动运算符会出现同样的问题:它将移动 text_ 的内容,但它也会移动指针,并且不知道它们是相对的。

您需要编写 copy/move 构造函数和赋值运算符,或者您可以考虑将 beg_end_ 替换为单个 size_t 大小值。

但在任何一种情况下,移动都不是你的朋友:你不是在转移所有权或执行浅拷贝,你的所有数据都在你的对象中。