C++中push_back和emplace_back的区别

简介: 在 `C++11` 之后,`vector` 容器中添加了新的方法:`emplace_back()` ,和 `push_back()` 一样的是都是在容器末尾添加一个新的元素进去,不同的是 `emplace_back()` 在效率上相比较于 `push_back()` 有了一定的提升。

1. push_back() 方法

首先分析较为简单直观的 push_back() 方法。对于 push_back() 而言,最开始只有 void push_back( const T& value ); 这个函数声明,后来从 C++11 ,新加了void push_back( T&& value ) 函数,以下为 C++ 中的源码实现:

/**
 *  以下程序来自STL源码 bits/stl_vector.h
 *
 *  @brief  Add data to the end of the %vector.
 *  @param  __x  Data to be added.
 *
 *  This is a typical stack operation.  The function creates an
 *  element at the end of the %vector and assigns the given data
 *  to it.  Due to the nature of a %vector this operation can be
 *  done in constant time if the %vector has preallocated space
 *  available.
 */
void push_back(const value_type &__x) {
    if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage) {
        // 首先判断容器满没满,如果没满那么就构造新的元素,然后插入新的元素
        _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
                                 __x);
        ++this->_M_impl._M_finish; // 更新当前容器内元素数量
    } else
        // 如果满了,那么就重新申请空间,然后拷贝数据,接着插入新数据 __x
        _M_realloc_insert(end(), __x);
}

// 如果 C++ 版本为 C++11 及以上(也就是从 C++11 开始新加了这个方法),使用 emplace_back() 代替
#if __cplusplus >= 201103L
void push_back(value_type &&__x) {
    emplace_back(std::move(__x));
}
#endif

C++20 之后,对这两个重载方法进行了修改,变成了 constexpr void push_back( const T& value ); 以及 constexpr void push_back( T&& value ); 。详情参考 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1004r2.pdf 版本修改计划

2. emplace_back() 方法

emplace_back() 是从 C++11 起新增到 vector 中的方法,最初的函数声明为:

template< class... Args >
void emplace_back( Args&&... args );

之后在 C++14 之后,将无返回值 void 改为了返回对插入元素的引用:

template< class... Args >
reference emplace_back( Args&&... args );

STL 源码中,可以看到 emplace_back() 的实现是这样的:

/**
 *  以下程序来自STL源码 bits/vector.tcc
 */
template<typename _Tp, typename _Alloc>
template<typename... _Args>
#if __cplusplus > 201402L
typename vector<_Tp, _Alloc>::reference
#else
void
#endif
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_realloc_insert(end(), std::forward<_Args>(__args)...);
#if __cplusplus > 201402L
    return back(); // 在 C++14版本之后,添加返回值,返回最后一个元素的引用
#endif
}

#endif

emplace_back()push_back() 中区别最大的程序拎出来看:

_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
                                 std::forward<_Args>(__args)...); // emplace_back()
_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
                                 __x);                            // push_back()

对于 std::forward() 函数而言,本质上是一个类型转换函数,它的声明函数如下所示:

/**
 *  以下程序来自STL源码 bits/move.h
 *  @brief  Forward an lvalue.
 *  @return The parameter cast to the specified type.
 *
 *  This function is used to implement "perfect forwarding".
 */
template<typename _Tp>
constexpr _Tp &&forward(typename std::remove_reference<_Tp>::type &__t) noexcept {
    return static_cast<_Tp &&>(__t);
}

在强制类型转换中,将参数 __t 传递给对应类 _Tp 的构造函数,然后调用了该类的构造函数从而完成对象创建过程。

因此,在 emplace_back() 函数中,是支持直接将构造函数所需的参数传递过去,然后构建一个新的对象出来,然后填充到容器尾部的。

3. 直观区别

声明一个 Person 类,里面只有一个字段 _age ,在容器中存储该类的对象,方便于查看整个函数调用过程。

class Person {
    int _age;

public:
    Person(int age) : _age(age) {
        cout << "Construct a person." << _age << endl;
    }

    Person(const Person &p) : _age(p._age) {
        cout << "Copy-Construct" << _age << endl;
    }

    Person(const Person &&p) noexcept: _age(p._age) {
        cout << "Move-Construct" << _age << endl;
    }
};

首先使用 push_back() 方法添加创建好的元素,可以看出使用到了拷贝构造函数

int main() {
    using namespace std;
    vector<Person> person;
    auto p = Person(1); // >: Construct a person.1
    person.push_back(p);
    /**
     * >: Copy-Construct1 因为容器扩容,需要把前面的元素重新添加进来,因此需要拷贝
     */
}

然后再使用 emplace_back() 函数添加元素进来:

int main() {
    using namespace std;
    vector<Person> person;
    auto p = Person(1); // >: Construct a person.1
    person.emplace_back(move(p)); // >: Move-Construct1
    person.emplace_back(2);
    /**
     * >: Construct a person.2  // 构建一个新的元素
     * >: Move-Construct1       // 拷贝之前的元素过来,这个时候用的是 Person(const Person &&p)
     */
}

可以看到直接使用构造参数列表来添加元素的方法,它会使用到了移动构造函数 move 。这也是 emplace_back() 方法的一大特色。

4. 性能分析

emplace_back() 函数在原理上比 push_back() 有了一定的改进,包括在内存优化方面和运行效率方面。内存优化主要体现在使用了就地构造(直接在容器内构造对象,不用拷贝一个复制品再使用)+强制类型转换的方法来实现,在运行效率方面,由于省去了拷贝构造过程,因此也有一定的提升。

以下程序源码:

/**
 * Created by Xiaozhong on 2020/9/3.
 * Copyright (c) 2020/9/3 Xiaozhong. All rights reserved.
 */

#include <vector>
#include <iostream>

using namespace std;

class Person {
    int _age;

public:
    Person(int age) : _age(age) {
        cout << "Construct a person." << _age << endl;
    }

    Person(const Person &p) : _age(p._age) {
        cout << "Copy-Construct" << _age << endl;
    }

    Person(const Person &&p) noexcept: _age(p._age) {
        cout << "Move-Construct" << _age << endl;
    }
};

#define TEST_EMPLACE_BACK
//#define TEST_PUSH_BACK

int main() {
    vector<Person> person;
    auto p = Person(1); // >: Construct a person.1
#ifdef TEST_EMPLACE_BACK
    person.emplace_back(move(p)); // >: Move-Construct1
    person.emplace_back(2);
    /**
     * >: Construct a person.2  // 构建一个新的元素
     * >: Move-Construct1       // 拷贝之前的元素过来,这个时候用的是 Person(const Person &&p)
     */
#endif
#ifdef TEST_PUSH_BACK
    person.push_back(p);
    /**
     * >: Copy-Construct1 因为容器扩容,需要把前面的元素重新添加进来,因此需要拷贝
     */
#endif
}
目录
相关文章
|
2月前
|
存储 C++ Cloud Native
云原生部署问题之C++ 中的 nullptr 和 NULL 区别如何解决
云原生部署问题之C++ 中的 nullptr 和 NULL 区别如何解决
40 0
|
3月前
|
存储 安全 C++
C++中的引用和指针:区别与应用
引用和指针在C++中都有其独特的优势和应用场景。引用更适合简洁、安全的代码,而指针提供了更大的灵活性和动态内存管理的能力。在实际编程中,根据需求选择适当的类型,能够编写出高效、可维护的代码。理解并正确使用这两种类型,是掌握C++编程的关键一步。
48 1
|
1月前
|
存储 编译器 C语言
C++内存管理(区别C语言)深度对比
C++内存管理(区别C语言)深度对比
58 5
|
2月前
|
Web App开发 Rust 分布式计算
Rust与C++的区别及使用问题之对于大量使用C++实现的产品来说,迁移到Rust的问题如何解决
Rust与C++的区别及使用问题之对于大量使用C++实现的产品来说,迁移到Rust的问题如何解决
|
2月前
|
Rust 安全 编译器
Rust与C++的区别及使用问题之Rust中的bound check对性能产生影响的问题如何解决
Rust与C++的区别及使用问题之Rust中的bound check对性能产生影响的问题如何解决
|
2月前
|
Rust 测试技术 编译器
Rust与C++的区别及使用问题之Rust项目中组织目录结构的问题如何解决
Rust与C++的区别及使用问题之Rust项目中组织目录结构的问题如何解决
|
1月前
|
缓存 C++ Windows
Inno setup 脚本判断 Microsoft Visual C++ Redistributable 不同版本区别
Inno setup 脚本判断 Microsoft Visual C++ Redistributable 不同版本区别
|
2月前
|
算法 Java C++
C++和Python在内存管理上的主要区别是什么?
【7月更文挑战第2天】C++和Python在内存管理上的主要区别是什么?
71 1
|
2月前
|
Rust 安全 程序员
Rust与C++的区别及使用问题之Rust解决多线程下的共享的问题如何解决
Rust与C++的区别及使用问题之Rust解决多线程下的共享的问题如何解决
|
2月前
|
Rust 编译器 程序员
Rust与C++的区别及使用问题之Rust避免多线程中的lifetime的问题如何解决
Rust与C++的区别及使用问题之Rust避免多线程中的lifetime的问题如何解决