美文网首页
我理解的vector、emplace_back与push_bac

我理解的vector、emplace_back与push_bac

作者: 王拓 | 来源:发表于2023-11-01 12:12 被阅读0次

    关于emplace_back和push_back网上有种说法,“emplace_back比push_back效率更高”,具体原因是emplace_back相比push_back减少了一次临时变量的拷贝构造。事实上大多数情况下,emplace_back和push_back表现是一致的。

    此外本文还验证了《Morden Effective C++》一书中条款14,“只要函数不会发射异常,就为其加上noexcept声明”。

    实验准备

    首先定义一个Element类型

    class Element{
    public:
        Element(){
            id_ = ++cnt_;
            std::cout << "default ctor " << id_ << std::endl;
        }
    
        ~Element(){
            std::cout << "dtor " << id_ << std::endl;
        }
    
        Element(int i){
            id_ = ++cnt_;
            std::cout << "ctor(int) " << id_ << std::endl;
        }
    
        Element(int i, char c){
            id_ = ++cnt_;
            std::cout << "ctor(int, char)" << id_ << std::endl;
        }
    
        Element(const Element& rhs){
            id_ = ++cnt_;
            std::cout << "copy ctor " << id_ << " from " << rhs.id_ << std::endl;
        }
        
        Element(Element&& rhs){
            id_ = ++cnt_;
            std::cout << "move ctor " << id_ << " from " << rhs.id_ << std::endl;
        }
    
        static int cnt_;
        int id_;
    };
    
    int Element::cnt_ = 0;
    

    Element类定义了不同的构造函数,静态变量cnt_用来为不同的实例的id_赋值。

    emplace_back和push_back

    插入左值

    首先测试插入左值的情况,下面是push_back的测试代码。

    std::vector<Element> vec;
    Element e;
    vec.push_back(e);
    

    下面是emplace_back的测试代码

    std::vector<Element> vec;
    Element e;
    vec.emplace_back(e);
    

    二者输出完全一致,可以看到当插入左值时,表现完全一致,都是调用拷贝构造。

    default ctor 1
    copy ctor 2 from 1
    dtor 1
    dtor 2
    

    插入右值

    首先测试插入默认构造函数返回的右值,首先是push_back版本

    std::vector<Element> vec;
    Element e;
    vec.push_back(e);
    

    下面是emplace_back版本

    std::vector<Element> vec;
    Element e;
    vec.emplace_back(e);
    

    两者输出完全一致,都是先调用构造函数,再调用移动构造

    default ctor 1
    move ctor 2 from 1
    dtor 1
    dtor 2
    

    换一个构造函数试试

    std::vector<Element> vec;
    vec.push_back(Element(1, 'a'));
    // vec.emplace_back(Element(1, 'a'));
    

    两者的输出同样完全一致,都是先调用构造函数,再调用移动构造

    ctor(int, char)1
    move ctor 2 from 1
    dtor 1
    dtor 2
    

    插入时构造

    区别在于emplace_back是使用可变参数模板定义的

    template<class... _Valty>
    void emplace_back(_Valty&&... _Val)
    

    当使用下面这样的插入形式时

    vec.emplace_back(1, 'a');
    // vec.push_back(1, 'a');
    

    push_back版本会报错,因为push_back没有实现可变参数。emplace_back的输出为

    ctor(int, char)1
    dtor 1
    

    可以看到,新的实例直接在vector内部调用了构造函数,省去了移动构造的步骤。

    要想调用push_back函数,则需要通过下面的代码。

    std::vector<Element> vec;
    vec.emplace_back(Element(1, 'a'));
    

    输出如下

    ctor(int, char)1
    move ctor 2 from 1
    dtor 1
    dtor 2
    

    如你所料,先后调用了构造函数和移动构造函数,这也是push_backemplace_back在插入时表现的唯一区别,而这个区别是由程序员编码方式产生的。

    验证noexcept

    先说结论,如果实现了不抛异常的移动构造函数vector扩容时会调用移动构造函数,否则调用拷贝构造函数

    参照Element类实现一个具有不抛异常的移动构造函数的ElementNoexp

    class ElementNoexp{
    public:
        ElementNoexp(){
            id_ = ++cnt_;
            std::cout << "default ctor " << id_ << std::endl;
        }
    
        ~ElementNoexp(){
            std::cout << "dtor " << id_ << std::endl;
        }
    
        ElementNoexp(int i){
            id_ = ++cnt_;
            std::cout << "ctor(int) " << id_ << std::endl;
        }
    
        ElementNoexp(int i, char c){
            id_ = ++cnt_;
            std::cout << "ctor(int, char)" << id_ << std::endl;
        }
    
        ElementNoexp(const ElementNoexp& rhs){
            id_ = ++cnt_;
            std::cout << "copy ctor " << id_ << " from " << rhs.id_ << std::endl;
        }
        
        ElementNoexp(ElementNoexp&& rhs) noexcept{
            id_ = ++cnt_;
            std::cout << "move ctor " << id_ << " from " << rhs.id_ << std::endl;
        }
    
        static int cnt_;
        int id_;
    };
    

    测试代码

    std::vector<ElementNoexp> vec;
    vec.resize(3);
    std::cout << "----------------------------------" << std::endl;    
    vec.emplace_back(ElementNoexp(1, 'a'));
    

    没有noexcept声明版本的输出

    default ctor 1
    default ctor 2
    default ctor 3
    ----------------------------------
    ctor(int, char)4
    move ctor 5 from 4
    copy ctor 6 from 1
    copy ctor 7 from 2
    copy ctor 8 from 3
    dtor 1
    dtor 2
    dtor 3
    dtor 4
    ----------------------------------
    dtor 6
    dtor 7
    dtor 8
    dtor 5
    
    

    noexcept版本输出

    default ctor 1
    default ctor 2
    default ctor 3
    ----------------------------------
    ctor(int, char)4
    move ctor 5 from 4
    move ctor 6 from 1
    dtor 1
    move ctor 7 from 2
    dtor 2
    move ctor 8 from 3
    dtor 3
    dtor 4
    ----------------------------------
    dtor 6
    dtor 7
    dtor 8
    dtor 5
    

    可以看到声明了noexcept版本移动构造函数的ElementNoexp在扩容过程中是边移动边析构,而没有声明noexcept的函数则是先确保拷贝构造完成,再释放原成员。

    因此,当确保移动构造不会抛出异常时,将移动构造声明为noexcept可以提高程序的执行效率。

    相关文章

      网友评论

          本文标题:我理解的vector、emplace_back与push_bac

          本文链接:https://www.haomeiwen.com/subject/loowidtx.html