Step 05

emplace_back / push_back

粘贴到作业纸 student/mini_vector/vector.hpp 底部不需要仓库,起步页已有全部工程文件

把本页「可粘贴代码」整段(从 template 到函数结尾)替换作业纸里对应的 TODO。不要贴进 class 体内。本步搜索 TODO(step 05),替换: push_back(const T&)、push_back(T&&)、emplace_back。参考答案在 参考答案页;编译问题见 常见问题。

Step 05 — emplace_back / push_back

emplace_back(args...) 用 args 就地构造 一个 T,返回新元素的引用(C++17 起 std::vector 也这样)。

push_back 只是它的两个包装。

有空槽:直接 construct

if (size_ < capacity_) {
    AllocTraits::construct(alloc_, data_ + size_, std::forward<Args>(args)...);
    ++size_;
    return data_[size_ - 1];
}

construct 抛了就不要 ++size_。这是强异常安全。

没空槽:先造新元素,再搬旧的

错误顺序:

  1. 分配新缓冲区
  2. 把旧元素 move 过去
  3. destroy 旧缓冲区
  4. 再用 args 构造新元素

如果 args 里有 v[0] 这种指向旧缓冲区的引用,第 3 步之后引用悬空。v.push_back(v[0]) 会炸。

正确顺序:

  1. 分配新缓冲区
  2. 在 new_data[size_] 上用 args 构造新元素(此时旧缓冲区还在,引用有效)
  3. 把旧的 size_ 个元素 relocate 到 new_data[0 .. size_)
  4. 析构并归还旧缓冲区
  5. data_ / capacity_ / size_ 一起提交

在作业纸里改哪里

搜索 TODO(step 05),替换 push_back(const T&)、push_back(T&&)、emplace_back。

emplace_back 是成员函数模板:作业纸里已有 template <typename... Args> 那一层,两层 template 都要保留。

可粘贴代码

template <typename T, typename Allocator>
void Vector<T, Allocator>::push_back(const T& value)
    requires std::copy_constructible<T>
{
    emplace_back(value);
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::push_back(T&& value) {
    emplace_back(std::move(value));
}
template <typename T, typename Allocator>
template <typename... Args>
typename Vector<T, Allocator>::reference
Vector<T, Allocator>::emplace_back(Args&&... args) {
    if (size_ < capacity_) {
        AllocTraits::construct(alloc_, data_ + size_, std::forward<Args>(args)...);
        ++size_;
        return data_[size_ - 1];
    }
 
    const size_type new_cap  = recommend_capacity(size_ + 1);
    T*              new_data = allocate_n(new_cap);
    T*              new_elem = nullptr;
    try {
        AllocTraits::construct(
            alloc_, new_data + size_, std::forward<Args>(args)...);
        new_elem = new_data + size_;
        uninitialized_relocate_n(new_data, data_, size_);
    } catch (...) {
        if (new_elem != nullptr) {
            AllocTraits::destroy(alloc_, new_elem);
        }
        deallocate_n(new_data, new_cap);
        throw;
    }
 
    T*        old_data = data_;
    size_type old_cap  = capacity_;
    size_type old_size = size_;
    data_              = new_data;
    capacity_          = new_cap;
    ++size_;
    destroy_range(old_data, old_data + old_size);
    deallocate_n(old_data, old_cap);
    return data_[size_ - 1];
}

uninitialized_relocate_n 自己会清掉搬迁到一半的元素。catch 里只要再拆掉那个已经造好的新元素,然后 deallocate 整块新内存。旧 vector 原封不动。

容量公式:recommend_capacity(size_ + 1),所以连续 push_back 是 1、2、4、8… 增长,均摊 O(1)。

如果编译器抱怨 push_back(const T&) 的 requires 和声明不一致:作业纸 class 里这一行必须是

void push_back(const T& value)
    requires std::copy_constructible<T>;

起步页提供的作业纸已经写好。不要删掉 requires。

验收

cmake --build build
./build/vector_tests --gtest_filter='Step05*'
./build/vector_tests --gtest_filter='MoveOnly.EmplaceAndPushRvalue'

重点看 PushBackSelfReferenceIsSafe:它会在反复扩容的情况下 push_back(v[0])。

本步验收

cmake --build build
./build/vector_tests --gtest_filter='Step05*'