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);
您可能还想使用
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
大小值。
但在任何一种情况下,移动都不是你的朋友:你不是在转移所有权或执行浅拷贝,你的所有数据都在你的对象中。
我发现了意外的(至少对我而言)行为。
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);
您可能还想使用
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
大小值。
但在任何一种情况下,移动都不是你的朋友:你不是在转移所有权或执行浅拷贝,你的所有数据都在你的对象中。