C++ 实现一个vector

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 guaranteenew_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;
}

)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/877112.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用xtrabackup对MySQL8.0.34进行备份和恢复

Percona XtraBackup 是一款开源的、用于 MySQL 和 MariaDB 的热备份工具,它可以在不停止数据库服务的情况下进行全量或增量备份,并且能够快速恢复数据。以下从特点、安装、备份与恢复操作、注意事项等方面进行详细介绍。 特点热备份:可以在数据库正常运行时进行备份,不影响…

【THM】Search Skills(搜索技巧)-学习

学习有效地搜索互联网并使用专门的搜索引擎和技术文档。本文相关的TryHackMe实验房间链接:https://tryhackme.com/r/room/searchskills 本文相关内容:学习有效地搜索互联网并使用专门的搜索引擎和技术文档。介绍 在 Google 上快速搜索“学习网络安全-learn cyber security”返…

使用ailabel对图片进行标注,vue3/js+ailabel.js

​ 一、实现效果 对方案可以添加多张图片,并在图片上进行标注。并且可以通过下方的缩略图切换方案图片 (demo) 二、效果图三、页面元素 <div class="w-full overflow-auto p-2" style="height: calc(100% - 7rem)"><div class="btns mb-2…

【模拟电子技术】17-基本放大电路的派生电路与场效应管放大电路的分析原则

【模拟电子技术】17-基本放大电路的派生电路与场效应管放大电路的分析原则 现在提出要求,要用NPN,PNP三极管各一个来构造放大电路,要求在大功率的时候,从输出端看都是PNP型,复合管就可以做到这一点可以看出第一个管子的功率肯定没第二个大,但是第一级管子决定了复合管的类…

Java Collection集合

目录集合概述集合框架Collection 常用功能 集合概述集合:集合是java中提供的一种容器,可以用来存储多个数据。 集合和数组既然都是容器,它们有啥区别呢? 数组的长度是固定的。集合的长度是可变的。 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对…

Java StringBuilder类

目录字符串拼接问题StringBuilder概述构造方法常用方法append方法toString方法 字符串拼接问题 由于String类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象。例如: public class StringDemo {public static void main(String[] args) {Strin…

[2025.1.28 MySQL学习] 锁

锁 全局锁全局锁就是对整个数据库实例加锁,加锁后处于只读状态,DML写语句、DDL语句、已经更新的事务提交语句都会被阻塞,典型使用场景时全库的逻辑备份 加全局锁:flush tables with read lock; 数据备份:mysqldump -uroot -p1234 itcast>itcast.sql 解锁:unlock table…

网络流量优化问题

问题描述 给定一个网络G(V, E) ,其中V为节点集合,E为链路集合。网络中的每条链路e的容量为Ce拓扑上的数字为链路的容量,假设网络中有K条单向网络流(k=n*(n-1),n为网络节点的数目),假定第i条网络流为fi,流的大小从[10, 100]区间中随机产生。现需要对K条网络流进行合理的…

8. Reading attributes 之 ATT_READ_BY_TYPE

1. ATT_READ_BY_TYPE_REQ 1.1 请求格式

Kafka 的部署(单机和集群)和 SpringBoot 访问

Kafka 由 Scala 和 Java 编写,最初由 LinkedIn 开发,后来成为 Apache 顶级项目,是一种高吞吐量的分布式发布/订阅消息系统。 Kafka 不仅仅是一个消息队列,还支持实时数据处理,其高吞吐、可扩展和持久化特性使其在大数据领域广泛应用。 本篇博客不详细介绍 Kafka,主要聚焦…

DeepSeek火爆全网,官网宕机?本地部署一个随便玩「LLM探索」

前言 最近 DeepSeek 狠狠刷了一波屏,国产大模型真的越来越厉害了👍,官方的服务器已经爆满了,以至于频繁出现反应迟缓甚至宕机的情况,和两年多之前 ChatGPT 的遭遇颇为相似。 我已经好久没有本地部署模型了(现在各厂商的模型都便宜量大),这次正好来试试 DeepSeek 开源模…

CF1000

A link首先,对于一个数(比如说\(x\)),它和它加一一定互质(也就是\(x\)和\(x+1\)一定互质),那么它和它加一组成的区间(\([x,x+1]\))一定是好区间,也一定是最小好区间,因为除了本身\([x,x+1]\)、两个数\([x,x]\),\([x+1,x+1]\)和空集不包含其他区间了,而相等两个数一…