移动语义,只是 C++11 提供的一种对象转移方法,程序可以通过移动语义的实现去降低对象转移成本,提高程序性能。
本文将结合测试例子走读 std::string 和 std::vector 对应源码,看看程序是如何通过移动语义方法去避免数据拷贝,提高程序性能的。
右值引用,移动语义,等详细知识可以参考:《Effective Modern C++》– 第五章
1. std::move
std::move 将对象类型被强制转换为右值引用。
1
2
3
4
5
6
7
/* bits/move.h */
template <typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept {
return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}
2. 源码分析
2.1. std::string

2.1.1. 移动构造
浅拷贝,实现了原对象成员数据转移到目标对象,原对象成员数据被重置。
- 测试源码。
1
2
3
4
5
6
7
8
9
/* g++ -g -O0 -W -std=c++11 test.cpp -o test -D_GLIBCXX_DEBUG && ./test */
#include <iostream>
int main() {
std::string s("1234567890123456789");
std::string ss(std::move(s)); /* 右值引用。*/
std::cout << s << " " << ss << std::endl;
return 0;
}
- stl 源码,移动构造逻辑简单,当数据量比较大时,可以避免深拷贝数据带来的开销。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* bits/basic_string.h */
template <typename _CharT, typename _Traits, typename _Alloc>
class basic_string {
...
/* 移动构造函数。*/
basic_string(basic_string&& __str) noexcept
: _M_dataplus(_M_local_data(), std::move(__str._M_get_allocator())) {
if (__str._M_is_local()) {
/* 参考:enum { _S_local_capacity = 15 / sizeof(_CharT) };
当原对象数据长度 <= 15,程序会跑到这里来。*/
traits_type::copy(_M_local_buf, __str._M_local_buf, _S_local_capacity + 1);
} else {
/* 字符串指针浅拷贝。*/
_M_data(__str._M_data());
_M_capacity(__str._M_allocated_capacity);
}
/* 设置当前字符串长度。 */
_M_length(__str.length());
/* 重置原数据。 */
__str._M_data(__str._M_local_data());
__str._M_set_length(0);
}
}
2.1.2. 复制构造
复制构造,申请新的空间,深拷贝数据。
- 测试源码。
1
2
3
4
5
6
7
8
9
/* g++ -g -O0 -W -std=c++11 test.cpp -o test -D_GLIBCXX_DEBUG && ./test */
#include <iostream>
int main() {
std::string s("1234567890123456789");
std::string ss(s); /* 复制构造,深拷贝数据。*/
std::cout << s << " " << ss << std::endl;
return 0;
}
- stl 源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/* bits/basic_string.h */
template <typename _CharT, typename _Traits, typename _Alloc>
class basic_string {
...
basic_string(const basic_string& __str)
: _M_dataplus(_M_local_data(),
_Alloc_traits::_S_select_on_copy(__str._M_get_allocator())) {
/* 构造分配空间,深拷贝数据。*/
_M_construct(__str._M_data(), __str._M_data() + __str.length());
}
template <typename _InIterator>
void
_M_construct(_InIterator __beg, _InIterator __end) {
typedef typename std::__is_integer<_InIterator>::__type _Integral;
_M_construct_aux(__beg, __end, _Integral());
}
template <typename _InIterator>
void
_M_construct_aux(_InIterator __beg, _InIterator __end,
std::__false_type) {
typedef typename iterator_traits<_InIterator>::iterator_category _Tag;
_M_construct(__beg, __end, _Tag());
}
}
/* bits/basic_string.tcc */
template <typename _CharT, typename _Traits, typename _Alloc>
template <typename _InIterator>
void basic_string<_CharT, _Traits, _Alloc>::
_M_construct(_InIterator __beg, _InIterator __end, std::forward_iterator_tag) {
...
size_type __dnew = static_cast<size_type>(std::distance(__beg, __end));
if (__dnew > size_type(_S_local_capacity)) {
/* 申请新的空间。*/
_M_data(_M_create(__dnew, size_type(0)));
_M_capacity(__dnew);
}
// Check for out_of_range and length_error exceptions.
__try {
/* 深拷贝数据。*/
this->_S_copy_chars(_M_data(), __beg, __end);
}
...
_M_set_length(__dnew);
}
template <typename _CharT, typename _Traits, typename _Alloc>
typename basic_string<_CharT, _Traits, _Alloc>::pointer
basic_string<_CharT, _Traits, _Alloc>::
_M_create(size_type& __capacity, size_type __old_capacity) {
...
/* 计算动态空间。*/
if (__capacity > __old_capacity && __capacity < 2 * __old_capacity) {
__capacity = 2 * __old_capacity;
// Never allocate a string bigger than max_size.
if (__capacity > max_size())
__capacity = max_size();
}
/* 申请空间。*/
return _Alloc_traits::allocate(_M_get_allocator(), __capacity + 1);
}
2.2. std::vector
接下来分析一下,动态数组容器是如何通过移动方式减少拷贝的。
- 测试源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* g++ -g -O0 -std=c++11 test.cpp -o test -D_GLIBCXX_DEBUG && ./test */
#include <iostream>
#include <vector>
int main() {
std::vector<std::string> v;
for (int i = 0; i < 5; i++) {
v.emplace_back(std::to_string(i));
}
std::cout << "--- no move ---" << std::endl;
std::vector<std::string> vv = v;
std::cout << "v size: " << v.size() << std::endl;
std::cout << "vv size: " << vv.size() << std::endl;
std::cout << "--- no move ---" << std::endl;
std::cout << "--- move ---" << std::endl;
std::vector<std::string> vvv = std::move(v); /* 右值引用。*/
std::cout << "v size: " << v.size() << std::endl;
std::cout << "vv size: " << vv.size() << std::endl;
std::cout << "vvv size: " << vvv.size() << std::endl;
std::cout << "--- move ---" << std::endl;
return 0;
}
- stl 源码,通过 gdb 调试方式,看看关键部分代码的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* /usr/include/c++/9/debug/vector */
template <typename _Tp, typename _Allocator = std::allocator<_Tp> >
class vector
: public __gnu_debug::_Safe_container<
vector<_Tp, _Allocator>, _Allocator, __gnu_debug::_Safe_sequence>,
public _GLIBCXX_STD_C::vector<_Tp, _Allocator>,
public __gnu_debug::_Safe_vector<
vector<_Tp, _Allocator>, _GLIBCXX_STD_C::vector<_Tp, _Allocator> > {
...
#if __cplusplus >= 201103L
...
vector(vector&&) noexcept = default;
...
#endif
...
}
/* /usr/include/c++/9/bits/stl_vector.h */
template <typename _Tp, typename _Alloc>
struct _Vector_base {
...
#if __cplusplus >= 201103L
_Vector_base(_Vector_base&&) = default;
#endif
...
struct _Vector_impl_data {
pointer _M_start; /* 目前使用空间头部位置。 */
pointer _M_finish; /* 当前使用空间尾部位置。 */
pointer _M_end_of_storage; /* 目前可用空间尾部位置。 */
#if __cplusplus >= 201103L
_Vector_impl_data(_Vector_impl_data&& __x) noexcept
/* 转移被转移对象的关键成员数据到当前对象。 */
: _M_start(__x._M_start), _M_finish(__x._M_finish), _M_end_of_storage(__x._M_end_of_storage) {
/* 被转移对象,关键成员数据被重置。 */
__x._M_start = __x._M_finish = __x._M_end_of_storage = pointer();
}
#endif
};
...
}
- 其它。有兴趣的朋友可以观测一下下面自定义结构的程序运行结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <vector>
struct A {
std::string s;
A(std::string str) : s(std::move(str)) { std::cout << "constructed\n"; }
A(const A& o) : s(o.s) { std::cout << "copy constructed\n"; }
A(A&& o) : s(std::move(o.s)) { std::cout << "move constructed\n"; }
~A() { std::cout << "destructed\n"; }
A& operator=(const A& other) {
s = other.s;
std::cout << " copy assigned\n";
return *this;
}
A& operator=(A&& other) {
s = std::move(other.s);
std::cout << " move assigned\n";
return *this;
}
};
int main() {
std::vector<A> v;
for (int i = 0; i < 5; i++) {
std::cout << i << " ---" << std::endl;
// v.push_back(std::to_string(i));
v.emplace_back(std::to_string(i));
}
std::cout << "--- no move ---" << std::endl;
std::vector<A> vv = v;
std::cout << "v size: " << v.size() << std::endl;
std::cout << "vv size: " << vv.size() << std::endl;
std::cout << "--- no move ---" << std::endl;
std::cout << "--- move ---" << std::endl;
/* 右值引用。*/
std::vector<A> vvv = std::move(vv);
std::cout << "v size: " << vv.size() << std::endl;
std::cout << "vv size: " << vv.size() << std::endl;
std::cout << "vvv size: " << vvv.size() << std::endl;
std::cout << "--- move ---" << std::endl;
return 0;
}
3. 小结
- 通过调试和走读 stl 源码,可以看到 std::string / std::vector 移动语义的实现,它们是如何影响程序性能的。
- 自定义的类实体对象,轻易不要传到 stl 容器里,因为你不知道里面有啥拷贝操作,反之应该传递对象指针,这样内部拷贝的成本就会比较低。
4. 参考
- 《Effective Modern C++》
- (ubuntu) vscode + gdb 调试 c++
- C++17剖析:string在Modern C++中的实现