Step 06

resize 与无默认构造

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

把本页「可粘贴代码」整段(从 template 到函数结尾)替换作业纸里对应的 TODO。不要贴进 class 体内。本步搜索 TODO(step 06) 和 TODO(step 01/06),替换: grow_buffer_and_append、resize(count)、resize(count, value)、Vector(count)、Vector(count, value)、Vector(initializer_list)。参考答案在 参考答案页;编译问题见 常见问题。

Step 06 — resize 与没有默认构造函数的 T

std::vector 有两个 resize:

void resize(size_type count);                 // 多出来的元素值初始化
void resize(size_type count, const T& value); // 多出来的元素拷贝 value

前者要求 T DefaultInsertable(对 std::allocator 来说就是能 T())。
struct Widget { Widget(int); }; 没有默认构造,v.resize(10) 必须无法编译,但 v.resize(10, Widget{1}) 可以。

C++20 用 requires 把无参 overload 从重载集里拿掉。对 Vector<NoDefault>,v.resize(3) 会直接找不到函数,而不是实例化到一半报一串内部错误。测试里有对应的 static_assert(!requires { ... })。

同样的约束加在 Vector(size_type count) 上。Vector(size_type count, const T& value) 不要加 default_initializable,它只需要可拷贝。

缩小

只析构尾巴,capacity 不变:

if (count < size_) {
    destroy_range(data_ + count, data_ + size_);
    size_ = count;
    return;
}

为了让抛异常时 capacity 也不变,需要重分配时走独立缓冲区:新元素先造好,再搬旧元素,最后才切换指针。容量足够时直接在尾巴上 construct 即可。

在作业纸里改哪里

搜索 TODO(step 06) 以及 TODO(step 01/06)。替换:

  • grow_buffer_and_append
  • resize(count)
  • resize(count, value)
  • Vector(count)
  • Vector(count, value)
  • Vector(initializer_list)

grow_buffer_and_append 是成员函数模板,保留两层 template。

可粘贴代码

template <typename T, typename Allocator>
template <typename TailCtor>
void Vector<T, Allocator>::grow_buffer_and_append(size_type count, TailCtor&& construct_tail) {
    const size_type new_cap  = recommend_capacity(count);
    const size_type appended = count - size_;
    T*              new_data = allocate_n(new_cap);
    bool            tail_ok  = false;
    try {
        construct_tail(new_data + size_, appended);
        tail_ok = true;
        uninitialized_relocate_n(new_data, data_, size_);
    } catch (...) {
        if (tail_ok) {
            destroy_range(new_data + size_, new_data + count);
        }
        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_              = count;
    destroy_range(old_data, old_data + old_size);
    deallocate_n(old_data, old_cap);
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::resize(size_type count)
    requires std::default_initializable<T>
{
    if (count < size_) {
        destroy_range(data_ + count, data_ + size_);
        size_ = count;
        return;
    }
    if (count == size_) {
        return;
    }
    if (count <= capacity_) {
        uninitialized_value_construct_n(data_ + size_, count - size_);
        size_ = count;
        return;
    }
    grow_buffer_and_append(count, [this](T* dest, size_type n) {
        uninitialized_value_construct_n(dest, n);
    });
}

带 value 的路径要小心别名:v.resize(100, v[0]) 里 value 引用的是 vector 自己的元素。重分配会让这个引用悬空,所以先拷到栈上。

template <typename T, typename Allocator>
void Vector<T, Allocator>::resize(size_type count, const T& value)
    requires std::copy_constructible<T>
{
    if (count < size_) {
        destroy_range(data_ + count, data_ + size_);
        size_ = count;
        return;
    }
    if (count == size_) {
        return;
    }
    T extra(value);
    if (count <= capacity_) {
        uninitialized_fill_n(data_ + size_, count - size_, extra);
        size_ = count;
        return;
    }
    grow_buffer_and_append(count, [this, &extra](T* dest, size_type n) {
        uninitialized_fill_n(dest, n, extra);
    });
}

三个构造函数

类外定义不要再写 = Allocator() 这种默认实参,默认实参只出现在 class 里的声明上。

template <typename T, typename Allocator>
Vector<T, Allocator>::Vector(size_type count, const Allocator& alloc)
    requires std::default_initializable<T>
    : alloc_(alloc) {
    if (count == 0) {
        return;
    }
    data_     = allocate_n(count);
    capacity_ = count;
    try {
        uninitialized_value_construct_n(data_, count);
        size_ = count;
    } catch (...) {
        deallocate_n(data_, capacity_);
        data_     = nullptr;
        capacity_ = 0;
        throw;
    }
}
template <typename T, typename Allocator>
Vector<T, Allocator>::Vector(size_type count, const T& value, const Allocator& alloc)
    requires std::copy_constructible<T>
    : alloc_(alloc) {
    if (count == 0) {
        return;
    }
    data_     = allocate_n(count);
    capacity_ = count;
    try {
        uninitialized_fill_n(data_, count, value);
        size_ = count;
    } catch (...) {
        deallocate_n(data_, capacity_);
        data_     = nullptr;
        capacity_ = 0;
        throw;
    }
}
template <typename T, typename Allocator>
Vector<T, Allocator>::Vector(std::initializer_list<T> init, const Allocator& alloc)
    requires std::copy_constructible<T>
    : alloc_(alloc) {
    const size_type count = init.size();
    if (count == 0) {
        return;
    }
    data_     = allocate_n(count);
    capacity_ = count;
    try {
        uninitialized_copy_n(data_, init.begin(), count);
        size_ = count;
    } catch (...) {
        deallocate_n(data_, capacity_);
        data_     = nullptr;
        capacity_ = 0;
        throw;
    }
}

带 value 的路径只要求 T 可拷贝构造,不要求默认构造。所以 Vector<NoDefault>(3, NoDefault{1}) 合法,Vector<NoDefault>(3) 非法。

验收

cmake --build build
./build/vector_tests --gtest_filter='Step06*'
./build/vector_tests --gtest_filter='NoDefaultCtor*'
./build/vector_tests --gtest_filter='Step01*'

本步验收

cmake --build build
./build/vector_tests --gtest_filter='Step06*:NoDefaultCtor*'