vector.emplace_back 调用构造函数

vector::emplace_back 会以出乎我们意料之外的方式,调用元素的析构函数和构造函数。

原因是因为 vector 在创建的时候,会申请一个内存,当内存不够的时候,申请更多的内存,然后调用构造函数,把原有的元素复制或者移动到新的内存上,然后析构掉原有的内存。

这里有一个例子:

#include <vector>
#include <iostream>
using namespace std;

class Foo {
  public:
    explicit Foo(int i) : i_{i}{
        std::cerr <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<<"]"//
                  << "this "  << (void*)this << " " //
                  << "i "  << i_ << " " //
                  << std::endl;
    }
#ifdef COPY_CTOR
    Foo(const Foo& other) : i_ {other.i_} {
        std::cerr <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<<"]"//
                  << "this "  << (void*)this << " " //
                  << "other.i "  << i_ << " " //
                  << std::endl;
    }
#endif
#ifdef MOVE_CTOR
    Foo(Foo&& other) NOEXCEPT : i_ {other.i_}  {
        std::cerr <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<<"]"//
                  << "this "  << (void*)this << " " //
                  << "other.i "  << i_ << " " //
                  << std::endl;
    }
#endif
    ~Foo() {
        std::cerr <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<<"]"//
                  << "this "  << (void*)this << " " //
                  << "i "  << i_ << " " //
                  << std::endl;
    }
    int i_;
};

int main(int argc, char *argv[])
{
    vector<Foo> v;
    v.emplace_back(0);
    Foo& v0 = v[0];
    cout << "before push: v.capacity() = " << v.capacity() << endl;
    cout << "v.size() = " << v.size() << endl;
    v.emplace_back(1);
    cout << "after push: v.capacity() = " << v.capacity() << endl;
    cout << "&v0 is dangling pointer: " << (void*)&v0 << endl;
    return 0;
}

我们只定义拷贝构造函数的情况下。

% g++ -DNOEXCEPT=  -DCOPY_CTOR -std=c++11 vector_data_race.cpp
vector_data_race.cpp:8: [Foo::Foo(int)]this 0x7fcdeb400690 i 0 
before push: v.capacity() = 1
v.size() = 1
vector_data_race.cpp:8: [Foo::Foo(int)]this 0x7fcdeb4006a4 i 1 
vector_data_race.cpp:15: [Foo::Foo(const Foo &)]this 0x7fcdeb4006a0 other.i 0 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fcdeb400690 i 0 
after push: v.capacity() = 2
&v0 is dangling pointer: 0x7fcdeb400690
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fcdeb4006a4 i 1 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fcdeb4006a0 i 0 

在调用 v1.push 之前, capacity = 1 , size = 1 ,这个时候在追加一个元素 v2 的时候,发生了以下事情:

  1. 内存空间不足了,需要申请新的空间。
  2. 申请新的空间之后,在新的空间上构造 v1 对象。
  3. 然后调用拷贝构造函数,把 v0 拷贝到新的内存上。
  4. 然后调用析构函数,析构掉 v0 对象。

这个时候, v0 依然拿着 v[0] 的引用,是悬空引用, 我们查看 v0 的内存地址,可以看到,在这个地址上,我们已经调用过了析构函数。

如果没有定义拷贝构造函数,而是定义移动构造函数,那么 vector 会调用拷贝构造函数。

% g++ -DNOEXCEPT=  -DMOVE_CTOR -std=c++11 vector_data_race.cpp
vector_data_race.cpp:23: [Foo::Foo(Foo &&)]this 0x7ff4a7c006a0 other.i 0 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7ff4a7c00690 i 0 
after push: v.capacity() = 2
&v0 is dangling pointer: 0x7ff4a7c00690
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7ff4a7c006a4 i 1 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7ff4a7c006a0 i 0 

如果同时定义拷贝构造函数和移动构造函数的话,vector 调用的是拷贝构造函数还是移动构造函数呢?

% g++ -DNOEXCEPT= -DCOPY_CTOR -DMOVE_CTOR -std=c++11 vector_data_race.cpp
vector_data_race.cpp:15: [Foo::Foo(const Foo &)]this 0x7fb2be4006a0 other.i 0 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fb2be400690 i 0 
after push: v.capacity() = 2
&v0 is dangling pointer: 0x7fb2be400690
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fb2be4006a4 i 1 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fb2be4006a0 i 0 

这里可以看到,vector 调用的是拷贝构造函数。这里让人比较吃惊。一般来说,移动构造函数是比拷贝构造函数高效的。

参考 https://en.cppreference.com/w/cpp/container/vector/emplace_back 中的描述,可以知道,这个是因为我们没有声明 noexcept

Exceptions
If an exception is thrown, this function has no effect (strong exception guarantee). If T's move constructor is not noexcept and is not CopyInsertable into *this, vector will use the throwing move constructor. If it throws, the guarantee is waived and the effects are unspecified.

我们试试看,声明移动构造函数为 noexcept

% g++ -DNOEXCEPT=noexcept -DCOPY_CTOR -DMOVE_CTOR -std=c++11 vector_data_race.cpp

我们得到下面的结果

vector_data_race.cpp:23: [Foo::Foo(Foo &&)]this 0x7fabe5c006a0 other.i 0 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fabe5c00690 i 0 
after push: v.capacity() = 2
&v0 is dangling pointer: 0x7fabe5c00690
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fabe5c006a4 i 1 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fabe5c006a0 i 0 

一定注意,如果想让 vector 调用元素的移动构造函数,一定要声明 noexcept