C++如何写一个带有allocator的vector

在STL中,容器一般都有一个allocator模板参数。

allocator用于获取/释放内存及构造/析构内存中元素的分配器。类型必须满足分配器 (Allocator) 。如果 Allocator::value_type 与 T 不同,那么行为未定义(C++20 前)程序非良构(C++20 起)。

模板声明

template <typename T, typename Allocator> 
class vector;

成员类型

一般容器中会有很多成员类型

template <typename T, typename Allocator> 
class vector
{
    using value_type = T;
    using allocator_type = Allocator;
    using size_type = std::size_t;
    using difference_type = std::ptrdiff_t;
    using reference = value_type&;
    using const_reference = const value_type&;
    using pointer = typename allocator_traits<Allocator>::pointer;
    using const_pointer = typename allocator_traits<Allocator>::const_pointer;

    // 标准中并没有规定不可以使用指针作为迭代器,在这里为了简化我们就不单独封装迭代器了。
    using iterator = T*; 

    // 标准库为我们提供了许多可用的组件让我们可以快速实现剩余的迭代器。
    using const_iterator = std::const_iterator<iterator>;
    using reverse_iterator = std::reverse_iterator<iterator>;
    using const_reverse_iterator = std::reverse_iterator<const_iterator>;
}

数据结构实现部分

我们可以将所有容器都分为两个部分,一个是实现部分,一个是allocator部分。

实现部分我们采用三根指针,如图一所示。

img

图1
struct storage
{
    pointer m_start = nullptr;           // 指向内存的首地址
    pointer m_finish = nullptr;          // 指向最后一个元素的下一个位置
    pointer m_end_of_storage = nullptr;  // 指向内存末尾的下一个位置

    constexpr storage() = default;

    constexpr storage(allocator_type& alloc, size_type cap)
    {
        // 申请内存
        m_start = alloc_traits::allocate(alloc, cap);
        // 设置状态
        m_finish = m_start;
        m_end_of_storage = m_start + cap;
    }

    constexpr void swap(storage& other) noexcept
    {
        using std::swap;
        swap(m_start, other.m_start);
        swap(m_finish, other.m_finish);
        swap(m_end_of_storage, other.m_end_of_storage);
    }
};
template <typename T, typename Allocator> 
class vector
{
    // ... 
private:
    storage m_store;

    [[no_unique_address]] /* allocator可能是一个空类 */ Allocator m_alloc;  
};

allocator需要配合标准库的std::allocator_traits一起使用。

using alloc_traits = std::allocator_traits<Allocator>;

销毁元素

struct storage
{
    // ...

    constexpr void clear(allocator_type& alloc)
    {
        for (; m_finish != m_start;)
        {
            // 直接逆向销毁元素即可。
            alloc_traits::destroy(alloc, --m_finish);
        }
    }

    constexpr void dispose(allocator_type& alloc)
    {
        if (m_start)
        {
            // 销毁元素
            clear(alloc);
            // 释放内存
            alloc_traits::deallocate(alloc, m_start, m_end_of_storage - m_start);
            // 重置状态
            m_start = m_finish = m_end_of_storage = nullptr;
        }
    }
};

扩容

struct storage
{
    // ...

    constexpr void expand_capacity_unchecked(allocator_type& alloc, size_t n)
    {

        storage new_buffer(alloc, n);

        // There are two situations:
        // 1. T models nothrow constructible.
        // 2. T does not model nothrow constructible.
        // We simply use a helper class to recover state if exception is thrown. 

        auto deleter = [&](storage* b) {
            b->dispose(alloc);
        };

        std::unique_ptr<storage, decltype(deleter)> guard(&new_buffer, deleter);

        auto& dest = new_buffer.m_finish;

        for (auto ptr = m_start; ptr != m_finish; ++ptr)
        {
            alloc_traits::construct(alloc, dest, std::move_if_noexcept(*ptr));
            dest++;
        }

        // If all elements are moved/copied into new_buffer successfully, we swap buffers and
        // dispose the current buffer.
        // If any exception is thrown when copying, following swap will not be invoked and
        // the guard will dispose the new buffer.
        new_buffer.swap(*this);
    }
};

内存分配器实现部分

C++容器中的allocator需要配合std::allocator_traits一起使用。具体的细节我们之前已经讨论难过了。

我们在这里简单编写一些可能会用的辅助函数:

template <typename T, typename Alloc>
auto rebind_allocator(Alloc& alloc)
{
    typename std::allocator_traits<Alloc>::template rebind_alloc<T> target(alloc);
    return target;
}

数据结构实现部分

我们可以将所有容器都分为两个部分,一个是实现部分,一个是allocator部分。

实现部分我们采用三根指针,如图一所示。

avatar

图1

我们专门去实现一个类来实现数据结构与算法部分。

template <typename T>
struct buffer
{
    using value_type = T;
    using pointer = T*;
    using const_pointer = const T*;

    using iterator = pointer;
    using const_iterator = const_pointer;

    template <typename Allocator>
    using rebind_traits = std::allocator_traits<Allocator>::template rebind_traits<T>;

    pointer m_start = nullptr;
    pointer m_finish = nullptr;
    pointer m_end_of_storage = nullptr;

    buffer() = default;
    buffer(const buffer&) = default; 
};

不同的是,我们这个buffer所有可能涉及内存的操作,都需要外界传入一个allocator。

template <typename T>
struct buffer
{
    buffer() = default;

    buffer(const buffer&) = default; 

    template <typename Allocator>
    buffer(Allocator& allocator, size_t size)
    {
        auto alloc = detail::rebind_allocator<T>(allocator);
        const auto sz = std::bit_ceil(size);
        m_start = rebind_traits<Allocator>::allocate(alloc, sz);
        m_finish = m_start;
        m_end_of_storage = m_start + sz;
    }
};

接下来我们补齐一些基本的接口。

constexpr size_t size() const
{
    return m_finish - m_start;
}

constexpr ssize_t ssize() const
{
    return static_cast<ssize_t>(size());
}

constexpr size_t capacity() const
{
    return m_end_of_storage - m_start;
}

constexpr size_t empty() const
{
    return size() == 0;
}

constexpr T& operator[](size_t n)
{
    return m_start[n];
}

constexpr const T& operator[](size_t n) const
{
    return m_start[n];
}

constexpr const T& front() const
{
    return *m_start;
}

constexpr T& front() 
{
    return *m_start;
}

constexpr const T& back() const
{
    return *(m_finish - 1);
}

constexpr T& back() 
{
    return *(m_finish - 1);
}

此时我们已经拥有了一个基本的雏形,接下来我们就需要提供第一个实际操作容器的方法: emplace_back。我们知道,当我们需要在末尾插入一个元素的时候,我们首先是需要确定当前的内存是否充足。如果当前的内存不足以让我们再插入新的元素时,我们需要进行扩容。

参见std::vector,我们将emplace_back定义成如下形式:

template <typename Allocator, typename... Args>
constexpr iterator emplace_back(Allocator& allocator, Args&&... args)
{
    auto alloc = detail::rebind_allocator<T>(allocator);

    if (...)
    {
        // 如果当前的内存已经不足以再放下元素时,我们先进行扩容。
    }

    return emplace_back_unchecked(alloc, (Args&&)args...);
}


template <typename Allocator, typename... Args>
constexpr iterator emplace_back_unchecked(Allocator& allocator, Args&&... args)
{
    auto alloc = detail::rebind_allocator<T>(allocator);
    rebind_traits<Allocator>::construct(alloc, m_finish, (Args&&)args...);
    // If an exceptions is thrown above, the m_finish will not increase.
    m_finish++;
    return m_finish;
}

扩容操作是我们第一个需要注意的操作,这个操作看起来很简单但是包含了许多一些注意的细节。扩容操作一般涉及以下几个步骤:

  • 申请一片新的内存
  • 将元素从旧的内存移动到新的内存
  • 释放旧内存
template <typename Allocator>
constexpr void expand_capacity_unchecked(Allocator& allocator, size_t n)
{
    assert(std::popcount(n) == 1);
    auto alloc = detail::rebind_allocator<T>(allocator);

    // 我们在这里申请一片新的内存
    buffer new_buffer(alloc, n);

    auto deleter = [&](buffer* b) {
        b->dispose(alloc);
    };

    // 我们使用unique_ptr来简单地帮助我们释放旧内存
    std::unique_ptr<buffer, decltype(deleter)> guard(&new_buffer, deleter);

    auto& dest = new_buffer.m_finish;

    // 接下来我们将每个元素移动到新的内存上
    for (auto ptr = m_start; ptr != m_finish; ++ptr)
    {
        // 按照C++标准来说,如果类型T的移动构造函数会抛出异常的话,我们将采用拷贝的
        // 策略来”移动“元素
        rebind_traits<Allocator>::construct(alloc, dest, std::move_if_noexcept(*ptr));
        dest++;
    }

    // 如果没有任何异常被抛出,那么我们交换一下buffer,然后unique_ptr的析构函数会帮助我们释放掉旧内存。
    // 如果有异常被抛出,那么下面这一行代码将不会被执行,此时unique_ptr的析构函数会帮助我们释放掉新内存。
    // 从而满足strong exception safety guarantee
    new_buffer.swap(*this);
}

注意到我们是以引用的形式接收参数的,所以我们不能够忽视参数的生命周期。

vector.emplace_back(vector.front());

如果vector此时发生了扩容,那么vector.front()就会指向一个失效的元素。所以我们必须先把参数保存起来然后才能够进行扩容。

if (m_finish == m_end_of_storage)
{
    // 我们利用alloc和args...先构造元素
    value_handle<value_type, decltype(alloc)> handle(alloc, (Args&&)args...);
    // 然后扩容
    expand_capacity_unchecked(alloc, std::bit_ceil(size() + 1));
    return emplace_back_unchecked(alloc, *handle);
}

从buffer末尾删除一个元素就比较简单了。

template <typename Allocator>
constexpr void pop_back(Allocator& allocator)
{
    assert(size() > 0 && "Empty buffer.");
    auto alloc = detail::rebind_allocator<T>(allocator);
    rebind_traits<Allocator>::destroy(alloc, --m_finish);
}

插入元素也是同理:

template <typename Allocator, typename... Args>
constexpr iterator emplace(Allocator& allocator, const_pointer position, Args&&... args) 
{
    assert(m_start <= position && position <= m_finish && "invalid position");
    auto alloc = detail::rebind_allocator<T>(allocator);

    value_handle<T, Allocator> handle(alloc, (Args&&) args...);

    // 我们记录一下位置,因为可能扩容之后迭代器会失效
    const size_t dist = position - m_start; 

    if (m_finish == m_end_of_storage)
    {
        expand_capacity_unchecked(alloc, std::bit_ceil(size() + 1));
    }

    if (dist == size())
    {
        return emplace_back_unchecked(alloc, (Args&&) args...);
    }
    else
    {
        auto dest = m_start + dist;

        // 需要注意的是,最后一个元素处于未初始化状态,所以我们在这里需要处理一下
        rebind_traits<Allocator>::construct(alloc, m_finish, std::move(*(m_finish - 1)));
        std::move_backward(dest, m_finish - 1, m_finish);
        rebind_traits<Allocator>::destroy(alloc, dest);
        rebind_traits<Allocator>::construct(alloc, dest, *handle);
        ++m_finish;
        return dest;
    }
}

删除元素总是非常简单的:

template <typename Allocator>
constexpr iterator erase(Allocator& allocator, const_iterator pos)
{
    assert(m_start <= pos && pos < m_finish && "Invalid position");
    auto alloc = detail::rebind_allocator<T>(allocator);

    iterator dest = const_cast<iterator>(pos);
    std::move(dest + 1, m_finish, dest);
    rebind_traits<Allocator>::destroy(alloc, --m_finish);
    return dest;
}

完整代码可以点击这里