[STL专题] 容器的自我保护机制

发布于 27 天前  134 次阅读


STL 中的容器可以说是我们日常开发中使用频率最高的组件了。我们知道,容器内的对象是存储在堆区的,当我们调用如push_back这类接口时,如果容器实例中存储的元素数量达到了容器当前最大值的时候,即obj.size() == obj.capacity()时,需要重新分配存储区,那么原来的存储区的对象该作何处理呢,对象是如何拷贝的呢?

Copy or Move

我们知道,从 C++11 开始,一个新的语法特性 move semantic 诞生了,显然,相比于移动操作,拷贝是昂贵的,特别是当存储的元素是一个很大的对象时,那么 C++ 的 STL 容器是如何处理这种场景的呢?

Without a noexcept

// created by tekky on 2021.1.30.

# include <iostream>
# include <vector>

struct Base {
  Base()              { std::cout << "constructor called" << std::endl; }
  Base(Base const& b) { std::cout << "copy constructor called" << std::endl; }
  Base(Base&& b)      { std::cout << "move constructor called" << std::endl; }
};

int main() {
  Base b{};
  std::vector<Base> bv = { b };
  std::cout << "bv.size() = " << bv.size()
            << "bv.capacity() = " << bv.capacity() << std::endl;
  // what will happen if we push now?
  bv.push_back(b);
}

从测试结果可以看到,这边调用 push_back 方法时,发生了两次拷贝构造,显然其中一次是为了将当前对象存储到 vector 所管理的堆区中时发生的拷贝,那么另外一次就是为了将旧存储区中的对象拷贝到新区时进行的拷贝了。

实测结果

With a noexcept

注意这边并没有做大面积改动,仅仅对移动构造器添加了 noexcept 限定符。

// created by tekky on 2021.1.30.

# include <iostream>
# include <vector>
 
struct Base {
  Base()                  { std::cout << "constructor called" << std::endl; }
  Base(Base const& b)     { std::cout << "copy constructor called" << std::endl; }
  Base(Base&& b) noexcpet { std::cout << "move constructor called" << std::endl; }
};
 
int main() {
  Base b{};
  std::vector<Base> bv = { b };
  std::cout << "bv.size() = " << bv.size() << "\t"
            << "bv.capacity() = " << bv.capacity() << std::endl;
  // what will happen if we push now?
  bv.push_back(b);
}

可以看到这一次 STL 终于理解到了我们的苦口婆心,使用了我们准备的移动构造器,将原来旧的对象移动至新的存储区,减少了一次拷贝操作,操碎了心的男妈妈泪目~

添加 noexcept 限定符后的结果

Why?

从最初的测试结果来看,明明我们创建了移动构造,为什么 STL 却要一意孤行使用拷贝版本呢。这其实是由于 STL 自身的安全策略,STL 执行移动语义的前提是它所存储的对像是可以安全移动的,也就是说如果 STL 在执行扩容操作时可能没有成功分配到内存,这时会抛出 bad_alloc 异常,如果你的类型不是安全的,将可能导致未定义的行为。


一只在互联网躬耕的菜鸟,写代码是热爱,二次元也是,mikoto也是