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 的时候,发生了以下事情:
- 内存空间不足了,需要申请新的空间。
- 申请新的空间之后,在新的空间上构造 v1 对象。
- 然后调用拷贝构造函数,把 v0 拷贝到新的内存上。
- 然后调用析构函数,析构掉 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