天天看點

C++中push_back和emplace_back的差別1. push_back() 方法2. emplace_back() 方法3. 直覺差別4. 性能分析

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
}           

繼續閱讀