我知道,除非另有说明,否则接受rvalue引用参数的所有标准库函数都保证将move-from参数保留为有效但未指定的状态,并且此处的某些示例可能会显示未定义的行为结果,但基本问题并不依赖于此。
以下计划:
// testmove1.cpp
#include <iostream>
#include <string>
int main() {
std::string s1{"String"};
std::cout << "s1: [" << s1 << "]" << std::endl;
std::string s2{std::move(s1)};
std::cout << "s2: [" << s2 << "]" << std::endl;
std::cout << "s1 after move: [" << s1 << "]" << std::endl; // Undefined
return 0;
}
输出:
paul@local:~$ ./testmove1
s1: [String]
s2: [String]
s1 after move: []
paul@local:~$
移动后输出s1
对我来说似乎未定义,但字符串留空至少是一个可行的选择。 Valgrind报告为该程序进行了单一分配,这正是您所期望的。
如果我做了一些非常相似的事情,但是有了一个班级成员,我会得到不同的结果:
// testmove2.cpp
#include <iostream>
#include <string>
class MyClass {
std::string m_data;
public:
MyClass(const std::string& data) :
m_data(data) {}
MyClass(const MyClass&& other) :
m_data{std::move(other.m_data)} {};
const std::string& get_data() const { return m_data; }
};
int main() {
MyClass c1{"Object"};
std::cout << "c1: [" << c1.get_data() << "]" << std::endl;
MyClass c2{std::move(c1)};
std::cout << "c2: [" << c2.get_data() << "]" << std::endl;
std::cout << "c1 after move: [" << c1.get_data() << "]" << std::endl;
return 0;
}
带输出:
paul@local:~$ ./testmove2
c1: [Object]
c2: [Object]
c1 after move: [Object]
paul@local:~$
清除第二个字符串的Not 似乎也是一个可行的选择,所以这本身并不令人惊讶。令我惊讶的是,这种行为完全不同于将字符串放入类中的结果。 Valgrind还报告了在这里进行的单一分配。
为了测试他们是否真的指向同一件事,我可以在移动后更改c2
,并检查c1
是否也发生了变化:
// testmove3.cpp
#include <iostream>
#include <string>
class MyClass {
std::string m_data;
public:
MyClass(const std::string& data) :
m_data(data) {}
MyClass(const MyClass&& other) :
m_data{std::move(other.m_data)} {};
const std::string& get_data() const { return m_data; }
void change_data() { m_data[0] = 'A'; }
};
int main() {
MyClass c1{"Object"};
std::cout << "c1: [" << c1.get_data() << "]" << std::endl;
MyClass c2{std::move(c1)};
std::cout << "c2: [" << c2.get_data() << "]" << std::endl;
std::cout << "c1 after move: [" << c1.get_data() << "]" << std::endl;
c2.change_data();
std::cout << "c1 after change: [" << c1.get_data() << "]" << std::endl;
std::cout << "c2 after change: [" << c2.get_data() << "]" << std::endl;
return 0;
}
输出:
paul@local:~$ ./testmove3
c1: [Object]
c2: [Object]
c1 after move: [Object]
c1 after change: [Object]
c2 after change: [Abject]
paul@local:~$
在这里,两个对象显然没有指向同一个东西,因为更改c2
不会影响c1
中存储的内容。 Valgrind现在报告2次分配,这似乎显然有必要解释观察到的行为,因为我们显然有两个不同的字符串,但对我来说并不明显为什么我们突然得到2个分配纯粹是因为改变其中一个。如果我完全摆脱了这一行动,只需创建c1
,然后在其上调用change_data()
,我就会按照您的预期获得1次分配。
我们可以通过删除移动后对c1
的所有访问来消除未定义的行为(除了我的任何其他错误):
// testmove4.cpp
#include <iostream>
#include <string>
class MyClass {
std::string m_data;
public:
MyClass(const std::string& data) :
m_data(data) {}
MyClass(const MyClass&& other) :
m_data{std::move(other.m_data)} {};
const std::string& get_data() const { return m_data; }
void change_data() { m_data[0] = 'A'; }
};
int main() {
MyClass c1{"Object"};
std::cout << "c1: [" << c1.get_data() << "]" << std::endl;
MyClass c2{std::move(c1)};
std::cout << "c2: [" << c2.get_data() << "]" << std::endl;
c2.change_data();
std::cout << "c2 after change: [" << c2.get_data() << "]" << std::endl;
return 0;
}
输出:
paul@local:~$ ./testmove4
c1: [Object]
c2: [Object]
c2 after change: [Abject]
paul@local:~$
我们显然不会再看到c1
没有改变的事实,因为我们没有输出它。但Valgrind仍显示2次分配。
任何人都知道发生了什么事,在这里?
为什么std::string
似乎在移动后自动归零,但不是在它是班级成员时?
在最后一个例子中,当我移动一个对象然后更改它时,为什么我得到两个分配而不是一个,当我移动对象时只获得一个分配然后不更改它?我知道我们似乎正朝着量子计算方向发展,但是在C ++中使用不确定性原则似乎还为时过早。
我正在使用g ++ 4.7.2,但我在clang-503.0.40上得到了相同的观察行为。
编辑:想一想,如果最终有两个对象处于有效状态,那么它们都有分配的意义。这只是编译器在确定其中一个分配永远不会被使用时优化其中一个分配的吗?如果是的话,构造一个最小例子是一种烦人的危险。
答案 0 :(得分:5)
我认为这是由于:
MyClass(const MyClass&& other) :
^^^^^
由于无法通过此引用更改绑定到other
的对象,因此预期移动操作的效果仅为副本。如果我删除此const
,则行为会更改回您的预期:
$ g++ -o tm3 tm3.cc -std=c++11 && ./tm3
c1: [Object]
c2: [Object]
c1 after move: []
答案 1 :(得分:0)
ad 1.我怀疑MyClass c2{std::move(c1)};
将默认的复制构造函数调用为
std::move(c1)
导致rvalue-reference而不是const rvalue-reference。然后,MyClass的默认复制构造函数调用std :: string的复制构造函数。
但是,如果MyClass(const MyClass&& other)
被调用,则它不会调用std :: string的move构造函数,因为std::move(other.m_data)
的类型为const std::string &&
。
不同类型的引用的匹配规则不容易记忆和应用,但实现资源转移的典型模式是使用带有非const rvalue引用的构造函数作为参数,因为资源旨在被带走。
ad 2. std :: string通常使用引用计数和copy-on write实现(如果计算了多个引用)。这可以解释你的观察结果。