F´ (F Prime) FifoQueueBase 抽象基类深度解析:FIFO 队列的接口设计与实现原理
【免费下载链接】fprimeF´ - A flight software and embedded systems framework项目地址: https://gitcode.com/GitHub_Trending/fpr/fprime
FifoQueueBase是 F´ 飞行软件框架 Fw/DataStructures 中 FIFO 队列家族的抽象基类模板,定义了"先进先出(FIFO)"队列的统一接口与公共行为。本文以该基类文档为骨架,结合 FifoQueueBase.hpp、ExternalFifoQueue.hpp 与 FifoQueue.hpp 的真实源码,系统讲解其模板参数、继承关系、构造策略、纯虚接口与公共函数实现,并给出基于Success返回码的完整使用示例,帮助你在嵌入式与飞行软件项目中正确实现与使用 FIFO 队列。
1. 设计定位:FIFO 队列家族的抽象基类
FifoQueueBase是一个 C++ 类模板,定义于Fw/DataStructures(命名空间Fw),表示一个 FIFO 队列的抽象基类。F´ 的Fw/DataStructures目录提供了一组基础数据结构库,其中 FIFO 队列家族由三个模板构成:
| 模板 | 描述 | 存储位置 |
|---|---|---|
ExternalFifoQueue | 使用外部内存存储队列项的 FIFO 队列 | 外部(调用方提供) |
FifoQueue | 使用内部内存存储队列项的 FIFO 队列 | 内部(对象自身持有) |
FifoQueueBase | FIFO 队列的抽象基类 | — |
从 sdd.md 中的类图可见,SizedContainer位于整个数据结构体系的最顶端,FifoQueueBase派生于它,而ExternalFifoQueue与FifoQueue又均公开继承FifoQueueBase:
基类本身不持有任何队列存储,而是把"队列"这一抽象形态的接口与公共算法固化下来,由派生类负责具体的内存布局与索引管理——这正是"抽象基类 + 内部/外部存储两种实现"这一设计模式的核心价值。
2. 模板参数与继承关系
2.1 模板参数
FifoQueueBase只有一个模板参数,即队列中元素的类型:
| 种类 | 名称 | 用途 |
|---|---|---|
typename | T | 队列中一个元素的类型 |
2.2 基类:SizedContainer
FifoQueueBase<T>公开派生于SizedContainer(见 FifoQueueBase.hpp)。SizedContainer是"有容量与大小的容器"抽象,它定义了三个纯虚函数和两个基于它们的公共便捷函数(见 SizedContainer.hpp):
| 函数 | 类型 | 语义 |
|---|---|---|
void clear() | 纯虚 | 清空容器 |
FwSizeType getSize() const | 纯虚 | 当前存储的元素个数(size) |
FwSizeType getCapacity() const | 纯虚 | 最大可存储元素个数(capacity) |
bool isEmpty() const | 非虚 | 返回getSize() == 0 |
bool isFull() const | 非虚 | 返回getSize() >= getCapacity() |
概念约定(源自 sdd.md):size是数据结构当前存储的元素数,capacity是最大可存储元素数。对固定大小数组二者相同,对 FIFO 队列等容器二者通常不同。这一对概念贯穿整个
Fw/DataStructures库。
FifoQueueBase继承这些接口后,不新增自己的抽象容器操作(clear、getSize、getCapacity仍由SizedContainer提供),而是新增队列特有的操作:enqueue、dequeue、at、peek、copyDataFrom。
3. 构造与析构策略:防拷贝、可多态
3.1 私有拷贝构造函数(已删除)
FifoQueueBase(const FifoQueueBase<T>& queue)在基类中被定义为= delete。基类删除拷贝构造的目的是:禁止通过基类引用对未知派生类型执行"浅拷贝",从而避免派生类内部存储(如环形缓冲区索引、外部存储指针)被错误复制。真正的拷贝语义由派生类各自实现(见第 6 节的operator=)。
3.2 受保护的零参数构造函数
FifoQueueBase()受保护(protected),使用成员默认初始化。它仅初始化基类SizedContainer(),不分配任何存储,保证只有派生类可以构造它(见 FifoQueueBase.hpp)。
3.3 虚析构函数
virtual ~FifoQueueBase()定义为= default。将析构函数声明为virtual是删除拷贝构造后的多态基类的标准做法,确保通过FifoQueueBase<T>*/&删除派生类对象时能正确调用派生类析构函数。
3.4 已删除的赋值运算符
FifoQueueBase& operator=(const FifoQueueBase&)同样定义为= delete(见 FifoQueueBase.hpp)。源码注释说明了原因:"We avoid virtual user-defined operators"——C++ 不允许虚的operator=,若在基类中开放赋值,会因静态类型信息丢失而无法正确复制派生状态,因此干脆在基类层禁止,由各派生类提供类型安全的赋值。
4. 纯虚接口:队列的三项基本操作
FifoQueueBase定义了三个纯虚函数,构成 FIFO 队列的最小操作集,派生类必须实现:
4.1 at —— 按下标取元素(不做移除)
virtual const T& at(FwSizeType index) const = 0返回指定索引处的元素。约定:索引 0 是队列最左端(最早入队)的元素,索引递增方向为从左到右。若索引越界,则触发断言失败(FW_ASSERT)。
at不修改队列状态,返回const T&引用,可零拷贝地读取元素。派生类 ExternalFifoQueue.hpp 中的实现印证了越界断言逻辑:
const T& at(FwSizeType index) const override { FW_ASSERT(index < this->m_size, static_cast<FwAssertArgType>(index), static_cast<FwAssertArgType>(this->m_size)); auto ci = this->m_dequeueIndex; const auto i = ci.increment(index); return this->m_items[i]; }注意它内部通过CircularIndex从m_dequeueIndex出发偏移index步,因此即使队列经历过多次出队(队首不在物理下标 0 处),at(0)也始终正确指向逻辑上最早的元素。
4.2 enqueue —— 入队(加到右端)
virtual Success enqueue(const T& e) = 0FifoQueueBase规定的语义如下:
- 初始化
status = Success::FAILURE; - 若队列有容纳新元素的空间,则把
e添加到队列右端,置status = Success::SUCCESS; - 返回
status。
注意入队失败(队列已满)不触发断言,而是返回Success::FAILURE让调用方决策。这也解释了为什么文档示例中都要检查返回值:
void f(FifoQueueBase<U32>& queue) { queue.clear(); auto status = queue.enqueue(3); ASSERT_EQ(status, Success::SUCCESS); }4.3 dequeue —— 出队(从最左端移除)
virtual Success dequeue(T& e) = 0- 初始化
status = Success::FAILURE; - 若
size > 0,则移除队列最左端的元素并存入e,置status = Success::SUCCESS; - 返回
status。
空队列出队同样返回Success::FAILURE而非断言,符合"查询/操作失败通过返回值表达"的 F´ 风格:
void f(FifoQueueBase<U32>& queue) { queue.clear(); U32 val = 0; auto status = queue.dequeue(val); ASSERT_EQ(status, Success::FAILURE); // 空队列出队失败 status = queue.enqueue(3); ASSERT_EQ(status, Success::SUCCESS); status = queue.dequeue(val); ASSERT_EQ(status, Success::SUCCESS); ASSERT_EQ(val, 3); }接口设计小结:
enqueue/dequeue用Success返回码报告"满/空"这种业务性失败;at用断言报告"越界"这种编程错误。这一分工是理解整个队列接口的关键。
5. 公共非虚函数:基类固化的算法
与纯虚接口不同,copyDataFrom和peek在基类中给出了完整实现(模板方法模式),所有派生类直接复用:
5.1 copyDataFrom —— 拷贝另一队列的全部数据
void copyDataFrom(const FifoQueueBase<T>& queue)算法步骤(与 FifoQueueBase.hpp 实现一一对应):
- 若
&queue != this(自拷贝直接忽略):- 调用
clear()清空自身; - 令
size = min(queue.getSize(), getCapacity()),即只拷贝能容纳的部分; - 对
i从 0 到size-1:- 令
e = queue.at(i); - 调用
enqueue(e)并断言status == Success::SUCCESS。
- 令
- 调用
由于第 2 步已保证size ≤ getCapacity(),拷贝过程中enqueue必然成功,所以第 3 步的断言是"永真"的防御性检查。源码中还把断言失败时的status一并传入FW_ASSERT作为调试参数:
const auto status = this->enqueue(e); FW_ASSERT(status == Fw::Success::SUCCESS, static_cast<FwAssertArgType>(status));示例(源队列元素多于目标容量时只拷贝容量上限个):
void f(FifoQueueBase<U32>& q1, FifoQueueBase<U32>& q2) { q1.clear(); // Enqueue an item U32 value = 42; (void) q1.enqueue(value); q2.clear(); ASSERT_EQ(q2.getSize(), 0); q2.copyDataFrom(q1); ASSERT_EQ(q2.getSize(), 1); }copyDataFrom是FifoQueue::operator=与拷贝构造的底层支撑(见第 6.2 节),实现了"跨不同存储布局实现之间拷贝队列数据"的能力。
5.2 peek —— 偷看指定索引元素而不移除
Success peek(T& e, FwSizeType index = 0) const参数index默认值为 0,即默认查看队首元素。算法:
- 初始化
status = Success::FAILURE; - 若
index < getSize(),则令e = at(index),置status = Success::SUCCESS; - 返回
status。
与at的关键区别:peek先做范围检查,越界返回FAILURE,绝不触发断言,适合在"不确定队列是否足够深"时安全地探测:
void f(FifoQueueBase<U32>& queue) { queue.clear(); U32 value = 0; auto status = queue.peek(value); ASSERT_EQ(status, Success::FAILURE); // 空队列 peek 失败 status = queue.enqueue(3); status = queue.peek(value); ASSERT_EQ(status, Success::SUCCESS); ASSERT_EQ(value, 3); // peek 到队首 3 status = queue.peek(value, 1); ASSERT_EQ(status, Success::FAILURE); // 索引 1 尚不存在 status = queue.enqueue(4); status = queue.peek(value, 1); ASSERT_EQ(status, Success::SUCCESS); ASSERT_EQ(value, 4); // peek 到第二个元素 4 }6. 派生类实现:环形缓冲区的底层原理
FifoQueueBase的接口之美在于:at(0)必须始终是"最早的元素",这要求存储层用环形(circular)缓冲区而非普通数组。两个派生类都复用了CircularIndex——一个"按模数回绕的索引"模板。
6.1 ExternalFifoQueue:外部存储实现
ExternalFifoQueue.hpp 的私有成员揭示了核心数据结构:
ExternalArray<T> m_items = {}; // 存放队列元素的数组(外部存储) CircularIndex m_enqueueIndex = {}; // 入队游标 CircularIndex m_dequeueIndex = {}; // 出队游标 FwSizeType m_size = 0; // 当前元素个数- 入队:在
m_enqueueIndex处写入元素后递增游标、m_size++; - 出队:读
at(0)(即从m_dequeueIndex开始偏移 0),递增出队游标、m_size--; - 清空:两个游标归零、
m_size = 0(见clear())。
构造函数通过setStorage接收类型化存储(T* items+capacity)或非类型化存储(ByteArray+capacity),并配套静态函数getByteArrayAlignment()/getByteArraySize(capacity)供调用方预先计算所需的字节数与对齐——这让队列可以被放到共享内存、内存池等场景中。
6.2 FifoQueue:内部存储实现
FifoQueue.hpp 是final类模板FifoQueue<T, C>,把ExternalFifoQueue与一块内嵌数组组合起来,并做了两个static_assert编译期约束:
static_assert(std::is_default_constructible<T>::value, "T must be default constructible"); static_assert(C > 0, "capacity must be greater than zero");即:元素类型必须可默认构造(因为内部数组T m_items[C]需要默认构造全部元素),容量必须大于 0。其成员为:
| 名称 | 类型 | 用途 | 默认值 |
|---|---|---|---|
m_extQueue | ExternalFifoQueue<T> | 外部队列实现 | C++ 默认初始化 |
m_items | T[C] | 为m_extQueue提供后备内存的数组 | C++ 默认初始化 |
零参数构造函数用ExternalFifoQueue<T>(m_items, C)把内嵌数组交给外部实现作后备存储;拷贝构造与operator=则全部转发给m_extQueue.copyDataFrom(queue),从而获得FifoQueueBase::copyDataFrom中"先清空、再按容量截断拷贝"的语义。所有虚接口(clear/enqueue/dequeue/at/getSize/getCapacity)都只是转发调用。
6.3 类的整体接口速查
| 成员 | 类别 | 说明 |
|---|---|---|
at(index) | 纯虚 | 返回索引处元素,越界断言 |
enqueue(e) | 纯虚 | 右侧入队,满返回FAILURE |
dequeue(e) | 纯虚 | 左侧出队,空返回FAILURE |
peek(e, index=0) | 非虚 | 越界安全地查看元素 |
copyDataFrom(q) | 非虚 | 先清空后按容量拷贝 |
clear()/getSize()/getCapacity() | 继承自SizedContainer | 容器通用操作 |
isEmpty()/isFull() | 继承自SizedContainer | 便捷判定 |
7. 测试验证:规则化单元测试
Fw/DataStructures的 FIFO 队列测试位于 Fw/DataStructures/test/ut/FifoQueueTest.cpp 与 Fw/DataStructures/test/ut/ExternalFifoQueueTest.cpp,采用规则(Rule)与场景(Scenario)驱动的 STest 测试框架,测试状态与规则定义在 Fw/DataStructures/test/ut/STest 目录下。
测试覆盖了基类文档中所有核心行为:
ZeroArgConstructor:构造后getCapacity() == C、getSize() == 0;CopyConstructor/CopyAssignmentOperator:拷贝后两个队列getSize()一致;CopyDataFrom:覆盖源队列元素数小于目标容量、等于目标容量、大于目标容量三种边界,验证copyDataFrom的截断语义;enqueue/dequeue/peek/at的成功与失败路径(含空队列出队失败、满队列入队失败、越界at触发断言等)。
例如CopyDataFrom测试中特意构造size1 > capacity2的情形,验证拷贝后目标队列恰好填满(见 FifoQueueTest.cpp),这直接对应文档第 5.1 节size = min(...)的算法步骤。
8. 工程应用:WasmSequencer 中的真实用法
在 F´ 源码中,Fw::FifoQueue有一个真实的使用案例:Svc/WasmSequencer组件用其缓存等待中的 WebAssembly 命令(见 Svc/WasmSequencer/WasmSequencer.hpp):
Fw::FifoQueue<WaitingCmd, WasmSequencerConfig::MAX_CONCURRENT_WAIT_COMMANDS> m_waiting;这里T是命令描述符WaitingCmd,容量来自配置常量MAX_CONCURRENT_WAIT_COMMANDS,充分体现了FifoQueue<T, C>"编译期确定容量、零动态内存分配"的嵌入式适用性。
需要特别强调的是线程安全边界(见 sdd.md):Fw/DataStructures的数据结构都是顺序数据结构,不支持多线程直接并发访问。若要在多线程上下文中使用,必须自行提供并发控制;最常见的做法是将其作为 active 或 queued 组件的成员,借助组件队列守护对数据结构的访问。F´ 组件内部队列的调度语义可参考Fw/Comp目录下的组件基类文档与 Fw/Comp 源码。
9. 从基类到实战:一份完整的使用模板
综合以上分析,使用FifoQueueBase家族的标准姿势如下:
#include "Fw/DataStructures/FifoQueue.hpp" // 内部存储实现(最常用) #include "Fw/Types/BasicTypes.hpp" // 1. 声明:元素类型 U32,容量 4 Fw::FifoQueue<U32, 4> queue; // 2. 入队:检查返回值,满则失败 auto status = queue.enqueue(10); FW_ASSERT(status == Fw::Success::SUCCESS); status = queue.enqueue(20); // 3. 通过基类引用进行多态访问 Fw::FifoQueueBase<U32>& base = queue; U32 front = 0; FW_ASSERT(base.peek(front) == Fw::Success::SUCCESS); // front == 10,不移除 FW_ASSERT(base.at(0) == 10); // 越界才会断言 // 4. 出队:先进先出 U32 e = 0; FW_ASSERT(base.dequeue(e) == Fw::Success::SUCCESS); // e == 10 FW_ASSERT(base.dequeue(e) == Fw::Success::SUCCESS); // e == 20 FW_ASSERT(base.dequeue(e) == Fw::Success::FAILURE); // 空队列 // 5. 容量相关查询(来自 SizedContainer) FW_ASSERT(queue.getCapacity() == 4); FW_ASSERT(queue.isEmpty());选择哪个实现取决于存储来源:
- 需要对象内嵌存储、栈上分配、确定性容量→
Fw::FifoQueue<T, C>; - 需要外部/共享存储(内存池、共享内存、非类型化字节缓冲)→
Fw::ExternalFifoQueue<T>(配合getByteArraySize计算缓冲大小); - 需要编写与具体实现无关的通用队列处理逻辑→ 只依赖
Fw::FifoQueueBase<T>接口,结合 ExternalFifoQueue 文档 与 FifoQueue 文档 理解各实现细节。
10. 小结
FifoQueueBase是 F´ 数据抽象体系中一个"小而精"的基类:它通过三个纯虚操作(at/enqueue/dequeue)+ 两个固化的算法(copyDataFrom/peek)+ 从SizedContainer继承的容量语义,把 FIFO 队列的共性完全收敛到基类,同时把存储布局的选择权留给派生类。配合CircularIndex实现的环形缓冲区,F´ 得以在零动态内存分配、确定性执行的前提下提供健壮的先进先出队列,这也正是其在 Svc/WasmSequencer 等飞行软件组件中被广泛采用的根本原因。
【免费下载链接】fprimeF´ - A flight software and embedded systems framework项目地址: https://gitcode.com/GitHub_Trending/fpr/fprime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考