【问题标题】:How to implement vector::clear()? (study purpose)如何实现vector::clear()? (学习目的)
【发布时间】:2020-11-26 07:26:03
【问题描述】:

我正在尝试实现自己的矢量类(用于学习目的)。目前我被困在vector::clear(),我该如何实现它?不可能是这样吧?

void clear() {
    for (size_t i = 0; i < _size; ++i) {
        _data = 0;//can't work with custom class
    }
}

或者

void clear() {
    delete[] _data;
    _data = new T[_cap];
    _size = 0;
}//can we make it better?

或者简单到:

void clear() {
    _size = 0;//fastest, but user can still access the data
}
T& operator[](size_t pos) {
    if (pos >= _size) throw;//added to prevent from accessing "cleared" data
    return _data[pos];
}

libcxx进行了一些挖掘,发现他们使用alloc_traits::destroy,这会破坏_data中的每个元素???
这是我的类属性

T* _data;
size_t _size;
size_t _cap;

【问题讨论】:

  • 如果您的目标是学习如何制作一些没有花里胡哨的东西,我通常不推荐标准库实现。与许多其他代码相比,它们具有非常特殊的要求,当您还不是专家时,这通常会使事情变得更加不堪重负。您将真正了解所有细节,但这在早期并不那么有用。
  • 请注意,在 C++20 之前,您不能可移植地重新实现 std::vector,迭代器/引用失效要求与没有 implicit object creationdata 上的要求不兼容
  • @Caleth afaik “为低级对象操作隐式创建对象”可追溯应用于所有标准。

标签: c++ vector


【解决方案1】:

clear 的唯一职责是在数组中的任何对象上调用析构函数并将数组大小设置为 0 (http://www.cplusplus.com/reference/vector/vector/clear/)。许多实现实际上并没有释放内存,而是在遍历向量并在每个元素上调用析构函数后简单地将数组中的 end 和 last 指针设置为开始指针。这是作为一种优化完成的,以便内存仍然可用,并且如果将新元素推送到向量上,向量就可以开始使用。

如果您不需要特定的优化,那么您只需删除[] 数据然后重新分配的 clear() 版本是完全合理的。这完全取决于您要做出哪些权衡。

【讨论】:

  • delete[] 正是你不能做的,因为这会在所有元素上调用析构函数,甚至是那些尚未构造的元素。
  • 取决于他的实现。如果他不是在构建对象,那是真的。但从那个版本的 clear() 看来,他总是将一个构造的对象数组保持在最大容量范围内。如果我们要进一步进行,也许需要原始海报中的更多代码?
  • 目前_data 总是用至少 1 个元素构造,以便于 push_back(如果超过 _cap:_cap * 2)。
  • “许多实现实际上并没有释放内存......作为优化完成” 他们这样做是因为标准 forces them to。它可能出于优化目的这样做,是的,但实现别无选择。
【解决方案2】:

如果您保留的容量大于大小,这意味着您需要考虑分配的不包含任何对象的内存。这意味着new T[_cap] 方法根本行不通:

  • 首先,您的向量不适用于不可默认构造的对象
  • 即使是这样,您也将创建比请求更多的对象,并且对于某些对象,构建成本可能很高。
  • 另一个问题是,当您push_back 时,当您有容量时,您将进行分配而不是构造对象(因为那里已经存在对象)

因此您需要将内存分配与对象创建分离

C++17 为此目的带来了一些实用函数,例如:std::uninitialized_default_constructuninitialized_copystd::destroy 等;在Dynamic memory management中查找更多信息

如果您想像std::vector 这样更通用,可以改用allocators


考虑到这一点,现在回答您关于clear 的具体问题。 std::vector::clear 的行为是:“从容器中删除所有元素。在此调用之后,size() 返回零。[...] 保持向量的 capacity() 不变”。如果您想要相同的行为,这意味着:

void clear() noexcept
{
    for (T* it = _data; it != _data + _size; ++it)
        it->~T();

    // or
    std::destroy(_data, _data + size);

    _size = 0;
}

如您所见,实现 std::vector 之类的东西绝非易事,需要一些专业知识和技术。

另一个复杂的来源来自strong exception safety。让我们仅考虑push_back 的情况作为示例。一个幼稚的实现可以做到这一点(伪代码):

void push_back(const T& obj)
{
    if size == capacity
         // grow capacity
         new_data = allocate memory
         move objects from _data to new_data
         _data = new_data
         update _cap

    new (_data + _size) T{obj}; // in-place construct
    ++_size;
}

现在想想如果一个对象的移动构造函数在移动到新的更大内存时抛出了会发生什么。您有内存泄漏,最糟糕的是:您的向量中的一些对象处于已移出状态。这将使您的向量处于无效的内部状态。这就是为什么std::vector::push_back 保证:

  1. 操作成功或
  2. 如果抛出异常,则函数无效。

换句话说,它保证它永远不会让对象处于“中间”或无效状态,就像我们的幼稚实现所做的那样。

【讨论】:

  • 关于push_back:1.new_data = allocate memory 2.std::copynew_data 3.std::swap这个是否提供强异常?
  • @Phineas 差不多。您需要确保不会泄漏内存。 stdlib 实现做类似的事情:如果Tnothrow_move_constructible,那么他们使用移动,否则他们回退到较慢的copy
猜你喜欢
  • 2021-02-12
  • 2020-04-02
  • 2018-11-19
  • 1970-01-01
  • 1970-01-01
  • 2013-12-15
  • 2019-12-03
  • 2018-08-01
  • 2010-10-18
相关资源
最近更新 更多