本文主要对右值引用的赋值语句的移动过程进行了探索,看看C++能不能自动帮我们处理好对象的“移动 ”操作。
实验一:对包含向量的类进行移动赋值
class Ref final
{
public:
std::vector<int> m_data;
std::string to_string() {
std::stringstream ss;
for (auto v: m_data) {
ss << v << ", ";
}
return ss.str();
}
};
void test_move_ref() {
new_section(__FUNCTION__, "");
Ref o;
Ref n;
o.m_data = std::vector<int>{1, 2, 3};
cout << "Ref before move: " << endl;
cout << " o: " << o.to_string() << endl;
cout << " n: " << n.to_string() << endl;
n = std::move(o);
cout << "Ref after move: " << endl;
cout << " o: " << o.to_string() << endl;
cout << " n: " << n.to_string() << endl;
}
类 Ref 只包含了一个数组,通过 std::move,试图将 o 的数据,转移给 n,得到的日志输出为
Ref before move:
o: 1, 2, 3,
n:
Ref after move:
o:
n: 1, 2, 3,
从结果来看,是完美做到了数据的移动的。
实验二:对级联包含向量的类进行移动赋值
class Cover final {
public:
Ref m_ref;
std::string to_string() {
std::stringstream ss;
ss << "cover(" << m_ref.to_string() << "), ";
return ss.str();
}
};
void test_move_cover() {
new_section(__FUNCTION__, "");
Cover o;
Cover n;
o.m_ref.m_data = std::vector<int>{1, 2, 3};
cout << "Cover before move: " << endl;
cout << " o: " << o.to_string() << endl;
cout << " n: " << n.to_string() << endl;
n = std::move(o);
cout << "Cover after move: " << endl;
cout << " o: " << o.to_string() << endl;
cout << " n: " << n.to_string() << endl;
}
类 Cover 只包含了一个 Ref,通过 std::move,试图将 o 的数据,转移给 n,得到的日志输出为
Cover before move:
o: cover(1, 2, 3, ),
n: cover(),
Cover after move:
o: cover(),
n: cover(1, 2, 3, ),
从结果来看,即使是在 Ref 外再包装一层,只要没有额外的成员,也是可以完美做到数据的移动的。
实验三:对包含向量和值的类进行移动赋值
class Mix final
{
public:
std::vector<int> m_data;
int m_index{0};
std::string to_string() {
std::stringstream ss;
for (auto v: m_data) {
ss << v << ", ";
}
ss << ": " << m_index;
return ss.str();
}
};
void test_move_mix() {
new_section(__FUNCTION__, "");
Mix o;
Mix n;
o.m_data = std::vector<int>{1, 2, 3};
o.m_index = 5;
cout << "Mix before move: " << endl;
cout << " o: " << o.to_string() << endl;
cout << " n: " << n.to_string() << endl;
n = std::move(o);
cout << "Mix after move: " << endl;
cout << " o: " << o.to_string() << endl;
cout << " n: " << n.to_string() << endl;
}
类 Mix 包含了一个数组,还有一个数值类型成员。通过 std::move,试图将 o 的数据,转移给 n,得到的日志输出为
Mix before move:
o: 1, 2, 3, : 5
n: : 0
Mix after move:
o: : 5
n: 1, 2, 3, : 5
从结果来看,数组这部分数据,是完美做到数据的移动的,然而数值类型成员,仅仅做到了拷贝。
实验四:对包含向量和指针的类进行移动赋值
class MixAddr final
{
public:
std::vector<int> m_data;
std::vector<int>* m_dynamic{nullptr};
std::string to_string() {
std::stringstream ss;
for (auto v: m_data) {
ss << v << ", ";
}
ss << ": ";
if (nullptr == m_dynamic) {
ss << "nullptr";
} else {
ss << m_dynamic << "(";
for (auto v: *m_dynamic) {
ss << v << ", ";
}
ss << ")";
}
return ss.str();
}
~MixAddr() {
if (nullptr != m_dynamic) {
// // deleting m_dynamic will cause program crash
// // because 'n = std::move(o)' only copy m_dynamic,
// // but only one instance of std::vector<int> is created,
// // so 'delete m_dynamic' will be executed twice,
// // this will trigger program crash
// delete m_dynamic;
m_dynamic = nullptr;
}
}
};
void test_move_mixaddr() {
new_section(__FUNCTION__, "");
MixAddr o;
MixAddr n;
o.m_data = std::vector<int>{1, 2, 3};
o.m_dynamic = new std::vector<int>{5, 6, 7};
cout << "MixAddr before move: " << endl;
cout << " o: " << o.to_string() << endl;
cout << " n: " << n.to_string() << endl;
n = std::move(o);
cout << "MixAddr after move: " << endl;
cout << " o: " << o.to_string() << endl;
cout << " n: " << n.to_string() << endl;
}
类 MixAddr 包含了一个数组,还有一个数组指针。通过 std::move,试图将 o 的数据,转移给 n,得到的日志输出为
MixAddr before move:
o: 1, 2, 3, : 0000024984E42370(5, 6, 7, )
n: : nullptr
MixAddr after move:
o: 1, 2, 3, : 0000024984E42370(5, 6, 7, )
n: 1, 2, 3, : 0000024984E42370(5, 6, 7, )
从结果来看,数组这部分数据没有移动,而是拷贝的;指针也是同样,仅仅做到了拷贝。
实验五:对级联包含向量和指针的类进行移动赋值
class Head final {
public:
Ref m_ref;
Ref* m_dynamic{nullptr};
std::string to_string() {
std::stringstream ss;
ss << "head(" << m_ref.to_string() << "), ";
if (nullptr == m_dynamic) {
ss << "nullptr";
} else {
ss << m_dynamic << "(" << m_dynamic->to_string() << ")";
}
return ss.str();
}
~Head() {
if (nullptr == m_dynamic) {
// // this should cause program crash like MixAddr, but not.
// // why? maybe latency of cleanup ?
delete m_dynamic;
m_dynamic = nullptr;
}
}
};
void test_move_head() {
new_section(__FUNCTION__, "");
Head o;
Head n;
o.m_ref.m_data = std::vector<int>{1, 2, 3};
o.m_dynamic = new Ref{};
o.m_dynamic->m_data = std::vector<int>{4, 5, 6};
cout << "Head before move: " << endl;
cout << " o: " << o.to_string() << endl;
cout << " n: " << n.to_string() << endl;
n = std::move(o);
cout << "Head after move: " << endl;
cout << " o: " << o.to_string() << endl;
cout << " n: " << n.to_string() << endl;
}
类 Head 包含了一个 Ref,还有一个 Ref 指针。通过 std::move,试图将 o 的数据,转移给 n,得到的日志输出为
Head before move:
o: head(1, 2, 3, ), 0000024984E42250(4, 5, 6, )
n: head(), nullptr
Head after move:
o: head(1, 2, 3, ), 0000024984E42250(4, 5, 6, )
n: head(1, 2, 3, ), 0000024984E42250(4, 5, 6, )
从结果来看,实验结果与实验四一致,Ref 数据没有移动,而是拷贝的;Ref 指针也是同样,仅仅做到了拷贝。
小结
通过实验来看,只包含对象(非指针引用)的类,应该可以不用定义 operator= (Ref&&),即可完成数据的转移 。
不过对于工程实践来讲,如果每个类都需要去分析他包含的成员变量再考虑移动的情况,那么代码的可读性和可维护性就比较差了。
综上,如果希望支持移动赋值方式,将数据转移,那么最好明确、完整的定义 operator=(Ref&&) ,至于构造函数 Ref(Ref&&),有空可能会再来一个实验做起来。
另外,本文只进行了对比实现,分析了现象,但是并未对原理和原因做深入探究,有兴趣的朋友欢迎在评论区分享一下,共同学习。
本文的所有示例,已同步到 Github,欢迎拉下来做测试和扩展实验。
如果对C++创建实例有兴趣,可以看看之前的另一篇文章:C++对象实例创建实验_DAVIED9的博客-CSDN博客