【问题标题】:std::move changes address of variables?std::move 改变变量的地址?
【发布时间】:2016-08-28 14:59:41
【问题描述】:

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

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 = '\0';
    }
    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

因为只有我移动这个类(使用std::move),并调用被移动对象的get_text(),所以我得到了垃圾,好像变量_text的移动地址改变了,所以_beg和@ 987654327@ 指向无处。

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

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

如果无法更改,是否会发生这种行为,因为我尝试将此类对象移动到 std::list(因此以某种方式发生了复制,它会更改变量的地址并使指针指向错误的位置)?

【问题讨论】:

  • 您的代码与std::move 无关。即使你不使用它也会有同样的问题。
  • _end = '\0'; 你的意思是*_end
  • @Michael,是的,你是对的,我在这里复制代码时出错
  • @Rakete1111:问题在代码中很明显。
  • @Arkady:不要重新输入代码。使用复制粘贴粘贴您在家中使用的文字、实际代码作为最小示例。如果代码太多,请重新编写最小示例以使其更小。

标签: c++ c++11 move


【解决方案1】:

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

当您移动一个对象时,您正在创建一个新对象,该对象从另一个对象获取其数据。会员地址不“变”;它只是不是同一个对象

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

一个即将被销毁的对象。

这就像搬进一所恰好与您的旧房子一模一样的房子。不管它看起来多么像你的老房子,它都不是。你仍然需要改变你的地址,因为它是一所新房子。 _beg_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
    }
};

这样,就不需要存储开始/结束指针。可以根据需要合成。

【讨论】:

  • 但是为什么它在第二次调用时返回垃圾?据我所知,成员变量没有改变。
  • _end_beg 仍然指向已被释放的原始对象。
【解决方案2】:

std::move 没有移动部件,它只是将输入参数提升为右值引用——请记住,在foo(T&amp;&amp; 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 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_的内容,但它也会移动指针,并且不知道它们是相对的。

您要么需要编写复制/移动构造函数和赋值运算符,要么可以考虑将 beg_end_ 替换为单个 size_t 大小值。

但无论哪种情况,move 都不是你的朋友:你没有转移所有权或执行浅拷贝,你的所有数据都在你的对象中。

【讨论】:

    猜你喜欢
    • 2017-08-21
    • 1970-01-01
    • 2013-08-16
    • 2020-07-30
    • 2011-05-20
    • 2018-04-17
    • 1970-01-01
    • 2018-08-26
    • 1970-01-01
    相关资源
    最近更新 更多