RIOT C++ 编码规范完整指南:从风格约定到源码实践
【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT
本指南系统梳理 RIOT(The friendly OS for IoT)在仓库根目录 CODING_CONVENTIONS_C++.md 中正式发布的 C++ 编码规范(其 Starlight 文档模板见 doc/starlight/templates/CODING_CONVENTIONS_CPP.template.md),并对照仓库内真实 C++ 源码给出可验证的落地示例。读完本文,你将掌握 RIOT 的 C++ 命名、头文件组织、换行规则、模板元编程格式与注释约定,能够写出风格统一、可被 uncrustify-riot.cfg 自动格式化、易于其他贡献者 review 的 RIOT C++ 代码。
规范定位与适用范围
RIOT 的 C++ 编码规范建立在C 语言编码规范的基础之上,文档开篇即明确:"You should check out the C Conventions as some section still apply (Documentation, Git, Travis)",即文档规范、Git 提交规范、CI(Travis/Murdock)要求等通用章节同时适用于 C 与 C++。C 语言规范全文位于仓库根目录 CODING_CONVENTIONS.md。
在风格取向上,RIOT 的 C++ 规范主要参考了Google C++ Style Guide与C++ 标准库的惯用写法,并在此基础上吸收了CAF(C++ Actor Framework)的编码风格,最终形成一套兼顾可读性与模板元编程可维护性的自家约定。其核心目标可以概括为:在 C++ 语法噪音(尤其是模板语法)不可避免的前提下,通过统一的格式约定让代码在视觉上"可被预测",降低 review 与维护成本。
快速示例(Example for the Impatient)
规范先给出了一对完整可编译的头文件/实现文件示例,几乎涵盖了后面所有章节的规则,适合"急性子"读者直接对照模仿。
头文件示例(约定路径module/riot/example/my_class.hpp):
// module/riot/example/my_class.hpp #ifndef RIOT_EXAMPLE_MY_CLASS_HPP #define RIOT_EXAMPLE_MY_CLASS_HPP #include <string> // use "//" for regular comments and "///" for doxygen namespace riot { namespace example { /// This class is only being used as style guide example. class my_class { public: /// Brief description. More description. Note that RIOT uses the /// "JavaDoc-style" autobrief option, i.e., everything up until the /// first dot is the brief description. my_class(); /// Destructs `my_class`. Please use Markdown in comments. ~my_class(); // suppress redundant @return if you start the brief description with "Returns" /// Returns the name of this instance. inline const std::string& name() const { return name_; } /// Sets the name of this instance. inline void name(const std::string& new_name) { name_ = new_name; } /// Prints the name to `STDIN`. void print_name() const; /// Does something (maybe). void do_something(); /// Does something else. void do_something_else(); private: std::string name_; }; } // namespace example } // namespace riot #endif // RIOT_EXAMPLE_MY_CLASS_HPP实现文件示例(my_module/my_class.cpp):
// my_module/my_class.cpp #include "riot/example/my_class.hpp" #include <iostream> namespace riot { namespace example { namespace { constexpr const char default_name[] = "my object"; } // namespace <anonymous> my_class::my_class() : name_(default_name) { // nop } my_class::~my_class() { // nop } void my_class::print_name() const { std::cout << name() << std::endl; } void my_class::do_something() { if (name() == default_name) { std::cout << "You didn't gave me a proper name, so I " << "refuse to do something." << std::endl; } else { std::cout << "You gave me the name " << name() << "... Do you really think I'm willing to do something " "for you after insulting me like that?" << std::endl; } } void my_class::do_something_else() { switch (default_name[0]) { case 'a': // handle a break; case 'b': // handle b break; default: handle_default(); } } } // namespace example } // namespace riot对照示例可以立刻读到几条关键信息:头文件宏保护采用<相对路径>_HPP形式;普通注释用//、Doxygen 注释用///;命名空间不增加缩进、注释中优先使用 Markdown;成员变量以_结尾、同名 getter/setter 不带后缀;类成员顺序为public→private;实现文件中被实现的头文件位于 include 列表最前。这些规则在下文各章节逐一展开。
通用规则(General)
缩进:每个缩进层级使用 4 个空格;每行最多 80 个字符;永远不要使用 Tab。
禁止 C 风格强制类型转换,一律使用 C++ 的
static_cast/reinterpret_cast/const_cast等。垂直空行只用于分隔函数,函数内部不使用空行切分逻辑块,改用注释标注逻辑段落。
文件后缀:头文件以
.hpp结尾,实现文件以.cpp结尾(与 C 规范的.h/.c区分开)。每行只声明一个变量,禁止
int a, b;这种写法。指针与引用绑定类型:写
const std::string& arg,而不是const std::string &arg。命名空间与访问修饰符不增加缩进层级(见上面示例中的
namespace riot与public:)。类内成员顺序固定为
public、protected、private。优先使用
auto,除非变量无法立即初始化,或你确实想要一次类型转换(此时必须加注释说明该转换的必要性)。禁止手动的裸资源管理,如裸
new/delete,应使用 RAII 与智能指针。禁止
typedef,一律写using T = X。关键字后跟空白:
if (...),template <...>,while (...)等。左花括号与语句同行(Allman 风格被明确排除):
void foo() { // ... }include 顺序固定,按"C 标准库 → C++ 标准库 → 第三方库 → 你自己的 RIOT 头文件"排列:
// some .hpp file #include <sys/types.h> #include <vector> #include "3rd/party.h" #include "riot/fwd.hpp"RIOT 自身头文件一律使用双引号,系统头文件使用尖括号。在
.cpp文件中,被实现的头文件必须排在最前面(保证自包含性检查),若需要平台相关头文件,可以紧接着第二位置包含riot/config.hpp。函数参数顺序:先输出(outputs)、后输入(inputs),与 STL 的参数约定保持一致。
单参数构造函数必须加
explicit,防止隐式类型转换。
命名规范(Naming)
除宏与模板参数外,所有名称一律小写并用下划线分隔(snake_case)。
模板参数名使用 CamelCase。
类型与变量使用名词;执行动作的函数使用"命令式动词";用于实现元编程的类也使用动词,例如
remove_const(对应std::remove_const的语义)。私有/受保护成员变量以
_结尾,同名 getter 与 setter 则使用不带后缀的名称:class person { public: const std::string& name() const { return name_ } void name(const std::string& new_name) { name_ = new_name; } private: std::string name_; };泛型模板参数约定:无约束的模板参数用
T,泛型函数实参用x;变参包(parameter pack)在二者后加s:template <class... Ts> void print(const Ts&... xs) { // ... }
头文件规范(Headers)
- 每个
.cpp文件必须有对应的.hpp文件,单元测试与main.cpp是仅有的例外。 - 每个类拥有独立的头文件/实现文件对,头文件的相对路径由其完整限定名推导:模块
my_module中的riot::example::my_class,其头文件位于path/to/my_module/riot/example/my_class.hpp,源文件位于path/to/my_module/my_class.cpp。这保证了头文件路径就是命名空间的镜像,#include时一目了然。 - 所有头文件使用
#define宏保护,宏名形式为<RELATIVE>_<PATH>_<TO>_<FILE>_HPP(即"相对路径转全大写 + 下划线")。例如上面示例中的RIOT_EXAMPLE_MY_CLASS_HPP。 - 能前向声明就不要
#include,以减小编译依赖。 - 每个库组件必须提供
fwd.hpp,前向声明用户 API 中用到的所有类型。 - 每个库组件必须提供
all.hpp,它承载该组件的文档主页,并#include用户 API 的全部头文件。 - 小函数使用
inline(经验法则:10 行以内)。
仓库中的实际 C++ 头文件正是这套规则的产物:以 sys/cpp11-compat/include/riot/thread.hpp 为例,其宏保护为RIOT_THREAD_HPP,头文件路径cpp11-compat/include/riot/thread.hpp完整对应riot::thread的命名空间与类名;同目录下的 sys/cpp11-compat/include/riot/mutex.hpp、sys/cpp11-compat/include/riot/chrono.hpp 也遵循"每个组件提供fwd.hpp/all.hpp"的约定,可通过cpp11-compat模块的 Makefile 进一步查看其组织方式。
语句换行(Breaking Statements)
构造函数初始化列表:需要换行时在逗号后换行,缩进 4 个空格,每个初始化器独占一行;能在一行放下的则不必换行:
my_class::my_class() : my_base_class(some_function()), greeting_("Hello there! This is my_class!"), some_bool_flag_(false) { // ok } other_class::other_class() : name_("tommy"), buddy_("michael") { // ok }函数参数:声明与调用均在逗号后换行,后续参数对齐到左括号之后:
intptr_t channel::compare(const abstract_channel* lhs, const abstract_channel* rhs) { // ... }运算符换行:在三元运算符与二元运算符之前换行(运算符留在行尾/下一行行首,见下方示例中
&&位于上一行行尾、条件延续到下一行行首对齐):if (today_is_a_sunny_day() && it_is_not_too_hot_to_go_swimming()) { // ... }
模板元编程格式(Template Metaprogramming)
模板最初并不是为编译期算法和类型变换设计的,因此 C++ 以大量的语法噪音惩罚元编程。RIOT 大量使用模板,为了让代码在噪音中保持可读,规范额外规定了几条元编程排版规则:
using name = ...一行放不下时,一律在=之后立即换行。考虑元编程函数的语义:例如
std::conditional本质是 if-then-else,那么 if 子句与两个分支各自独占一行。每"打开"一层模板就增加一级缩进,并将收尾的
>、>::type或>::value单独放在一行:using optional_result_type = typename std::conditional< std::is_same<result_type, void>::value, bool, optional<result_type> >::type; // think of it as the following (not valid C++): auto optional_result_type = conditional { if result_type == void then bool else optional<result_type> };注释中给出的"伪代码"直观展示了这种排版的意图:让
conditional的三个参数像真正的 if-then-else 语句一样分层对齐。普通类型别名不受此限制:面对"普通"模板时,按开
<的位置对齐即可,例如:using response_handle_type = response_handle<Subtype, message, ResponseHandleTag>;
预处理宏(Preprocessor Macros)
- 只有在无法用 inline 函数或常量达到同样效果时才使用宏。
- 宏命名形式为
RIOT_<COMPONENT>_<NAME>,与头文件宏保护的命名风格一致。例如上文示例中的RIOT_EXAMPLE_MY_CLASS_HPP,以及RIOT_THREAD_HPP。
注释规范(Comments)
- Doxygen 注释以
///开头(普通//注释不会被 Doxygen 吞掉,用于纯代码内部说明)。 - 优先使用 Markdown 而非 Doxygen 格式化指令,例如使用反引号包裹代码符号(见示例中的
`my_class`、`STDIN`)。 - 使用
@cmd而非\cmd,即写@param、@return、@brief。 - 若 brief 描述以 "Returns" 开头,可省略冗余的
@return。 - 注意 RIOT 启用了"JavaDoc-style" autobrief:brief 描述取到第一个句号为止,其后内容视为详细描述。
仓库源码同样大量采用这些注释约定,例如 sys/include/irq.hpp 中irq_lock类的注释使用了@brief、@details、@return指令与反引号代码标记;sys/cpp11-compat/include/riot/thread.hpp 中this_thread、thread等类也以@brief+ Markdown 风格撰写文档。
源码中的落地佐证:规范如何体现在真实代码里
RIOT 仓库中真实的 C++ 代码分布在sys/cpp11-compat、sys/include、examples、tests等目录。以下示例可以印证规范并非纸上谈兵:
- 命名空间组织与命名规则:sys/cpp11-compat/include/riot/thread.hpp 定义了
namespace riot,类thread、thread_id均为小写加下划线的名词;sys/include/irq.hpp 中class irq_lock同样是"名词类 + RAII"的典型写法,构造时irq_disable()、析构时irq_restore(state),正是"禁止裸new/delete、使用 RAII"规则的直接体现。 - 头文件宏保护:sys/cpp11-compat/include/riot/thread.hpp 使用
#ifndef RIOT_THREAD_HPP / #define RIOT_THREAD_HPP,结尾#endif // RIOT_THREAD_HPP,完全符合<相对路径>_HPP约定。 - 小函数 inline:
thread_id的比较运算符、thread::joinable()、thread::get_id()等短函数均声明为inline,与"10 行以内用 inline"的经验法则吻合。 - 禁止
typedef、使用using:sys/cpp11-compat/include/riot/thread.hpp 中using id = thread_id;、using native_handle_type = kernel_pid_t;是标准的别名写法。 - 每行一个变量、80 列约束:
thread_data结构体与thread类中的每个成员声明都独占一行;长参数列表(如 thread 构造函数实现 中的thread_create调用)在逗号后换行对齐,与"Breaking Statements"章节一致。
需要说明的是,仓库内既有代码与规范并非逐条完全一致(例如个别历史文件使用m_前缀成员变量或/** */注释风格),规范面向的是新提交代码与渐进式重构。作为贡献者,应以 CODING_CONVENTIONS_C++.md 为唯一准绳,配合仓库根目录 uncrustify-riot.cfg 的 uncrustify 配置自动完成 4 空格缩进、花括号位置、行宽等机械性检查,再人工对照命名、头文件组织、元编程排版与注释约定,即可保证代码风格与 RIOT 主线保持一致。
【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考