文先生的博客 求职,坐标深圳。(wenfh2020@126.com)

[stl 源码分析] 浅析 std::vector::emplace_back

2023-08-01

本文通过测试和走读 std::vector::emplace_back 源码,理解 C++11 引入的 emplace 新特性。

原理相对简单:emplace_back 函数的参数类型是可变数量的 万能引用,参数通过 完美转发 到 std::vector 内部进行对象创建构造,可以有效减少参数传递过程中产生临时对象,避免了对象的移动和拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* /usr/include/c++/4.8.2/debug/vector */
template <typename _Tp, typename _Allocator = std::allocator<_Tp> >
class vector : public _GLIBCXX_STD_C::vector<_Tp, _Allocator>,
               public __gnu_debug::_Safe_sequence<vector<_Tp, _Allocator> > {
    ...
    // emplace_back 参数是万能引用。
    template <typename... _Args>
    void emplace_back(_Args&&... __args) {
        ...
        // 完美转发传递参数。
        _Base::emplace_back(std::forward<_Args>(__args)...);
        ...
    }
#endif
    ...
};

1. 概述

std::vector::emplace_back 是 C++ 中 std::vector 类的成员函数之一,它用于在 std::vector 的末尾插入一个新元素,而不需要进行额外的拷贝或移动操作

具体来说,std::vector::emplace_back 函数接受可变数量的参数,并使用这些参数构造一个新元素,然后将其插入到 std::vector 的末尾,这个函数的优点是可以避免额外的拷贝或移动操作,从而提高性能。

文字来源:ChatGPT


2. 测试源码

  • 系统。
1
2
3
4
5
6
7
8
9
# cat /proc/version
Linux version 3.10.0-1127.19.1.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) 
(gcc version 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC) )

# g++ --version                                 
g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-44)
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  • 测试源码。
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
/* g++ -O0 -std=c++11 test.cpp -o test && ./test */
#include <iostream>
#include <vector>

class Data {
   public:
    Data(const std::string& str) {
        m_str = str;
        std::cout << m_str << " constructed" << std::endl;
    }
    Data(const Data& d) : m_str(d.m_str) {
        std::cout << m_str << " copy constructed"
                  << std::endl;
    }
    Data(Data&& d) : m_str(std::move(d.m_str)) {
        std::cout << m_str << " moved constructed" << std::endl;
    }
    Data& operator=(const Data& rhs) {
        if (this != &rhs) {
            m_str = rhs.m_str;
            std::cout << m_str << " copy assigned" << std::endl;
        }
        return *this;
    }
    Data& operator=(Data&& rhs) {
        if (this != &rhs) {
            m_str = std::move(rhs.m_str);
            std::cout << m_str << " move assigned" << std::endl;
        }
        return *this;
    }

   private:
    std::string m_str;
};

int main() {
    std::vector<Data> datas;
    datas.reserve(16);

    Data a("aa");
    datas.push_back(a);
    std::cout << std::endl;

    datas.push_back(Data("bb"));
    std::cout << std::endl;

    Data c("cc");
    datas.emplace_back(c);
    std::cout << std::endl;

    datas.emplace_back(Data("dd"));
    std::cout << std::endl;

    datas.emplace_back("ee");
    return 0;
}
  • 测试结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# g++ -O0 -std=c++11 test.cpp -o test && ./test 
aa constructed
aa copy constructed
-------------
bb constructed
bb moved constructed
-------------
cc constructed
cc copy constructed
-------------
dd constructed
dd moved constructed
-------------
ee constructed
-------------

3. STL 源码剖析

上面的测试结果反馈了一些有趣的信息:在对象元素的插入过程中,有的触发拷贝构造,有的触发移动构造,有的两者都没触发。为什么会这样呢?通过查看 emplace_back 的内部实现源码,我们将会找到答案。


通过走读源码:

  1. 我们可以发现 emplace_back 的输入参数类型是 万能引用,入参通过 完美转发 给内部 ::new 进行对象构造,并将其追加到数组对应的位置。

  2. 测试例程里 datas.emplace_back("ee");,它插入对象元素,并没有触发拷贝构造和移动构造。因为 emplace_back 接口传递的是字符串常量,而真正的对象构造是在内部实现的:::new ((void*)__p) _Up(std::forward<_Args>(__args)...); ,在插入对象元素的整个过程中,并未产生须要拷贝和移动的 临时对象

  • 万能引用参数类型 + 完美转发。

详细知识请查看《Effective Modern C++》- 第五章:右值引用、移动语义和完美转发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* /usr/include/c++/4.8.2/debug/vector */
template <typename _Tp, typename _Allocator = std::allocator<_Tp> >
class vector : public _GLIBCXX_STD_C::vector<_Tp, _Allocator>,
               public __gnu_debug::_Safe_sequence<vector<_Tp, _Allocator> > {
    ...
    // emplace_back 参数是万能引用。
    template <typename... _Args>
    void emplace_back(_Args&&... __args) {
        ...
        // 完美转发传递参数。
        _Base::emplace_back(std::forward<_Args>(__args)...);
        ...
    }
#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
/* /usr/include/c++/4.8.2/bits/vector.tcc */
#if __cplusplus >= 201103L
template <typename _Tp, typename _Alloc>
template <typename... _Args>
void vector<_Tp, _Alloc>::emplace_back(_Args&&... __args) {
    if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage) {
        _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
                                 std::forward<_Args>(__args)...);
        ++this->_M_impl._M_finish;
    } else {
        _M_emplace_back_aux(std::forward<_Args>(__args)...);
    }
}
#endif

/* /usr/include/c++/4.8.2/bits/alloc_traits.h */
template <typename _Tp, typename... _Args>
static typename enable_if<__construct_helper<_Tp, _Args...>::value, void>::type
_S_construct(_Alloc& __a, _Tp* __p, _Args&&... __args) {
    __a.construct(__p, std::forward<_Args>(__args)...);
}

template <typename _Tp, typename... _Args>
static auto construct(_Alloc& __a, _Tp* __p, _Args&&... __args)
    -> decltype(_S_construct(__a, __p, std::forward<_Args>(__args)...)) {
    _S_construct(__a, __p, std::forward<_Args>(__args)...);
}

/* /usr/include/c++/4.8.2/ext/new_allocator.h */
template <typename _Tp>
class new_allocator {
#if __cplusplus >= 201103L
    template <typename _Up, typename... _Args>
    void construct(_Up* __p, _Args&&... __args) {
        // 新建构造对象,并通过完美转发给对象传递对应的参数。
        ::new ((void*)__p) _Up(std::forward<_Args>(__args)...);
    }
#endif
};

4. 注意

本文测试用例调用了 std::vector::reserve 预分配了动态数组空间,如果没有这一行源码,我们将会看到不一样的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
aa constructed
aa copy constructed
-------------
bb constructed
bb moved constructed
aa copy constructed
-------------
cc constructed
cc copy constructed
aa copy constructed
bb copy constructed
-------------
dd constructed
dd moved constructed
-------------
ee constructed
aa copy constructed
bb copy constructed
cc copy constructed
dd copy constructed
-------------

因为动态数组,使用的是连续的内存空间,一些操作可能会触发内存的动态扩展,这个过程中可能产生数据拷贝或者移动。所以当我们不了解容器内部具体实现时,最好不要往容器里保存类/结构对象元素,保存 对象指针 是个不错的选择,即便容器内部发生数据拷贝,成本也比较低。

std::vector 内部内存扩展,元素对象为什么不是转移而是拷贝构造呢?我们应该为移动构造函数添加 noexcept 标识,这样才会进行移动,这里就不展开讨论了。


5. 引用