C++代码规范指南
@copyright horizon.ai
版本 | 修改人 | 评审人 | 修订时间 | 修订内容 |
---|---|---|---|---|
V1.00 | 施展 | - | 2021-08-05 | 评审版发布 |
V1.01 | 赖远志、陈培哲(1-3章)、诸家炜(4-6章)、施展(7-9章) | 软件TMG全员 | 2021-09-06 | 正式发布 |
目录
- C++代码规范指南
- 目录
- 正文
- 0 阅前说明
- 1 头文件
- 2 作用域
- 2.1 命名空间
- 2.1.1【建议】鼓励在 .cc 文件内使用匿名命名空间或 static 声明。使用具名的命名空间时, 其名称可基于项目名或相对路径
- 2.1.2【规范】禁止使用 using-directive(using namespace …),可以使用using type alias
- 2.1.3【规范】禁止使用内联命名空间(inline namespace)
- 2.1.4【规范】不要在头文件里定义命名空间别名(Namespace aliases)
- 2.1.5【规范】在source file中,
namespace
应该包裹住 除includes
,宏定义和其他namespace
中的前置声明之后的 所有代码 - 2.1.6【规范】不要在std命名空间中声明任何东西,包括前置声明。
- 2.2 内部连接性
- 2.3 非成员函数 静态成员函数和全局函数
- 2.4 局部变量
- 2.5 静态和全局变量
- 2.6 threadlocal Variables
- 2.1 命名空间
- 3 类
- 4 函数
- 5 来自Google的奇技
- 6 其他 C++ 特性
- 7 命名约定
- 8 注释
- 9 格式
- 9.1 行长度
- 9.2 非ASCII字符
- 9.3 空格还是制表位
- 9.4 函数声明和定义
- 9.4.1【规范】对显式重写的虚函数要使用override修饰。重写虚函数时不要添加virtual关键字
- 9.4.2【规范】所有形参应尽可能对齐,如果第一个参数无法和函数名放在同一行,则换行后的参数保持 4 个空格的缩进
- 9.4.3【建议】只有在参数未被使用或者其用途非常明显时, 才能省略参数名
- 9.4.4【建议】如果返回类型和函数名在一行放不下,分行
- 9.4.5【建议】如果返回类型与函数声明或定义分行了, 不要缩进
- 9.4.6【建议】左圆括号总是和函数名在同一行
- 9.4.7【建议】函数名和左圆括号间永远没有空格
- 9.4.8【建议】圆括号与参数间没有空格
- 9.4.9【建议】左大括号总在最后一个参数同一行的末尾处,不另起新行
- 9.4.10【建议】右大括号总是单独位于函数最后一行, 或者与左大括号同一行
- 9.4.11【建议】右圆括号和左大括号间总是有一个空格
- 9.5 Lambda表达式
- 9.6 浮点字面量
- 9.7 条件
- 9.8 循环
- 9.9 指针和引用表达式
- 9.10 布尔表达式
- 9.11 预处理指令
- 9.12 类格式
- 9.13 命名空间
- 9.14 变量和数组初始化
- 9.15 操作符
- 9.16 整体示例
正文
0 阅前说明
- 当前规范以 C++11标准 为基础进行说明。
- 目录组织参考 google cpp guide。
- 所有条例都是以 【规范】 或 【建议】 组织的,其中 规范 是指必须遵循的,而 建议 则是可以根据实际情况而讨论的。
- 每个条例上都有 【说明】、【代码示例】 进行补充说明。
1 头文件
1.1 Self-contained头文件
1.1.1【规范】头文件应该能够自给自足(self-contained),头文件本身依赖的其它头文件,需要全部包含(也就是可以作为第一个头文件被引入),以 .h或.hpp 结尾
【说明】
出于易维护性考虑,需要保证 在包含该头文件后,可以不需要引入其它头文件,就可以保证编译通过。事实上 cpplint 也会检查这条规则。
【代码示例】
注意 #include <string>
放的位置
// foo.h
#ifndef FOO_H_
#define FOO_H_
// print_str 接口中的入参是 string 类型
// 所以要求在 这加上 头文件引用
#include <string>
void print_str(const std::string& input);
#endif // A_H_
// foo.cpp
// 注意:如果只放在这虽然也能编译通过,但是 直接引用 foo.h 的地方就都需要 #include <string>
// #include <string>
#include "foo.h"
void print_str(const std::string& input) {
// do sth
}
1.1.2【规范】至于用来插入文本的文件,说到底它们并不是头文件,所以应以 .inc 结尾。不允许分离出 -inl.h 头文件的做法
1.1.3【规范】模板和内联函数的定义需要和其声明放在同一文件内
【说明】
模板是遵循谁用谁生成的原则,如果声明和定义分散在不同文件,那么在链接过程中就会报错。
1.1.4【规范】只引用直接使用的头文件,如果源文件或头文件引用了其他文件定义的符号,则应直接包含该头文件,从而提供该符号的声明或定义。不要依赖传递包含(transitive inclusions)
【说明】
这样开发者可以删除不再需要的 include
语句,而不会破坏其使用者。
【代码示例】
举个例子,如果 foo.cc
使用了 bar.h
定义的符号,则 foo.cc
应该直接包含 bar.h
。而不能因为 foo.h
包含了 bar.h
就省略。
1.2 头文件保护
1.2.1【建议】使用 #program once 来防止头文件多重包含
【说明】
与 #program once
对应 的就是 "使用 #define
来防止头文件被多重包含"。
后者的兼容性更好(gcc 低版本不支持 #program once
的语法特性 编译器兼容性),但通常会在文件操作或者编写过程中出现字符拼写问题。
此条标准是 “建议” 属性。但是仓库代码风格要保证一致。此外,对于 “使用 #define
来防止头文件被多重包含” 的,那么 命名格式当是: <PROJECT>_<PATH>_<FILE>_H_
【代码示例】
比如foo项目中的foo/src/bar/baz.h文件,其对应的头文件保护应为:
// #ifdef 用法
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_
// #pragram once
#pragma once
1.3 前置声明
1.3.1【建议】尽可能地避免使用前置声明,使用 #include 包含需要的头文件即可
【说明】
前置声明的优点:
- 缩短编译时间,
#include
使编译器打开了更多的文件,处理更多的文本。 - 避免不必要的重新编译。
缺点:
- 前置声明会隐藏依赖关系。
- 不利于自动化工具查找相关符号的定义。
- 前置声明不利于库的后续更新。函数和模板的前置声明阻止了库的所有者对其API做其他兼容性的修改,例如扩大参数类型、添加具有默认值的模板参数或迁移到新的命名空间。
- 针对命名空间std::的正向声明符号会产生未定义的行为。
- 很难界定是否只需要前置声明或者需要直接包含(
#include
)。将#include
修改成前置声明很可能默默的改变代码的含义,如下例:
// b.h:
struct B {};
struct D : B {};
// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); } // calls f(B*)
如果对 struct B
和 D
使用前置声明替代#include
,则 test
会调用 f(void *)
- 前置声明多个符号可能比简单地
#include
更冗长 - 使用前置声明可能导致代码速度变慢(例如,使用指针成员而不是对象成员)
1.4 内联函数
1.4.1【规范】只有当函数只有 10 行甚至更少时才将其定义为内联函数
【说明】
内联函数是指,编译器会将其内联展开,而不是按通常的函数调用机制进行调用。函数并不总是内联的,即使它们被声明为内联的;例如,虚函数和递归函数通常不是内联的。通常递归函数不应该是内联的。
内联函数的优点:
- 只要内联的函数体较小,内联该函数可以令目标代码更加高效。对于存取函数以及其它函数体比较短,性能关键的函数,鼓励使用内联。
缺点: - 滥用内联将导致程序变得更慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
1.4.2【建议】递归函数不应该设置成内联属性
【说明】
递归调用堆栈的展开并不像循环那么简单,比如递归层数在编译时可能是未知的, 因此大多数编译器都不支持内联递归函数。
1.4.3【建议】虚函数不应该设置成内联属性
【说明】
如果虚函数通过对象被调用,倒是可以 inlined
,但大部分虚函数调用动作是通过对象的指针或引用完成的,此类行为无法被 inlined
。因为 inlined
意味着编译期将调用端的调用动作被函数本体取代,若无法知道哪个函数该被调用时,编译器没法将该函数加以 inlining
。
1.4.4【建议】内联中应该避免使用 循环 或 switch 语句函数,除非在大多数情况下,这些循环或 switch 语句从不被执行
【说明】
带条件分支的内联函数使CPU更加难以准确地预测分支语句,因为分支的每个实例都是独立的。如果有多个分支语句,则成功的分支预测将比调用函数节省更多的周期。
除此之外,switch 意味着在每个展开的地方都需要做函数展开,都有个跳转表,那么会直接导致浪费代码段空间。
1.5 #include的路径及顺序
1.5.1【规范】使用标准的头文件包含顺序可增强可读性,避免隐藏依赖,格式依次按 相关头文件、C库、C++库、其它库 .h。 本项目内的.h 且每个区域用空行隔开,在每个部分中,包含的内容应按字母顺序排列
【说明】
把 “相关头文件” 放在最开始处,其实也是为了避免 “相关头文件” 中少了相应,而不自知。详见 1.1.1 描述。
【代码示例】
以 include/base/foo.h
和 src/foo.cc
为例,建议的 #include
顺序和格式如下。
// 相关头文件(.cc文件实现相对应的.h)
#include "base/foo.h"
// 空行
// C 库
#include <stdio.h>
// 空行
// C++ 库
#include <iostream>
// 空行
// 其他库的 .h
#include <glog/logging.h>
// 空行
// 本项目内的 .h
#include "hobot-adas/data-structure/base_type_def.h"
1.5.2【建议】避免使用 快捷目录 .(当前目录)或者 .. (上级目录)
1.5.3【规范】对于C库、C++库、第三方库采用#include <>
1.5.4【规范】一些系统相关的、需要使用条件包含的(#ifdef)代码,应该放在其他包含代码之后,同时保证相关代码简短
【代码示例】
#include "foo/public/fooserver.h"
#include "base/port.h" // For LANG_CXX11.
#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11
2 作用域
2.1 命名空间
2.1.1【建议】鼓励在 .cc 文件内使用匿名命名空间或 static 声明。使用具名的命名空间时, 其名称可基于项目名或相对路径
【说明】
【代码示例】
// In the .h file
namespace mynamespace {
// 所有声明都置于命名空间中
// 注意不要使用缩进
class MyClass {
public:
...
void Foo();
};
} // namespace mynamespace
// .cc 文件
namespace mynamespace {
// 函数定义都置于命名空间中
void MyClass::Foo() {
...
}
} // namespace mynamespace
2.1.2【规范】禁止使用 using-directive(using namespace …),可以使用using type alias
【说明】
using namespace
会直接让其内部所有的符号都变得可见。如果该库升级,且新版本有可能引入一个与应用使用的 符号 相同,那么会造成程序编译失败。
【代码示例】
// 禁止 —— 污染命名空间
// using namespace foo;
2.1.3【规范】禁止使用内联命名空间(inline namespace)
【说明】
所谓内联空间中的名字可以被外层命名空间直接使用,无需在内联命名空间的名字前添加表示命名空间的前缀,通过外层命名空间的名字就可以直接访问它,如下代码所示。
之所以禁止在于,内部成员不再受其声明所在命名空间的限制,而导出到外层了,这其实与命名空间作用是违背的。(内联命名空间只在大型版本控制 里用)
namespace A {
inline namespace inlineA1 {
void funcA1() {
cout << "inlineA1()" << endl;
}
}
inline namespace inlineA2 {
void funcA2() {
cout << "inlineA2()" << endl;
}
}
}
int main() {
// A::inlineA1::funcA1(); //不需要指定inlineA1
A::funcA2(); //inlineA2()
return 0;
}
2.1.4【规范】不要在头文件里定义命名空间别名(Namespace aliases)
【说明】
任何在头文件中引入的命名空间都会成为公开API的一部分
【代码示例】
// 在 .cc 中使用别名缩短常用的命名空间
namespace baz = ::foo::bar::baz;
// 在 .h 中使用别名缩短常用的命名空间
namespace librarian {
namespace impl { // 仅限内部使用
namespace sidetable = ::pipeline_diagnostics::sidetable;
} // namespace impl
inline void my_inline_function() {
// 限制在一个函数中的命名空间别名
namespace baz = ::foo::bar::baz;
...
}
} // namespace librarian
2.1.5【规范】在source file中,namespace
应该包裹住 除includes
,宏定义和其他 namespace
中的前置声明之后的 所有代码
【说明】
如果把 include
或者 前置声明 放在 namespace
中,语义更像是 这些头文件的变量或者前置声明的类型 都是属于 该 namespace
的,与实际不符。宏定义 并不受命名空间约束,引用的地方不需要使用命名空间前缀,所以也没必要放命名空间中。
【代码示例】
// In the .cc file
#include "xxx.h"
#define MACRO ""
namespace other_namespace {
class OtherClass;
} // namespace other_namespace
namespace mynamespace {
// Definition of functions is within scope of the namespace.
void MyClass::Foo() {
...
}
} // namespace mynamespace
2.1.6【规范】不要在std命名空间中声明任何东西,包括前置声明。
【说明】
这种做法的结果是未定义的,且不具备可移植性。应当直接引用相应的头文件。
2.2 内部连接性
2.2.1【规范】在 .cc 文件中定义一个不需要被外部引用的变量时,应该将它们放在匿名命名空间或声明为 static ,但是不要在 .h 文件中这么做
【说明】
【代码示例】
// *.cpp
static ...
namespace {
...
} // namespace
2.3 非成员函数 静态成员函数和全局函数
2.3.1【规范】使用静态成员函数或命名空间内的非成员函数,尽量不要用裸的全局函数
【说明】
裸的全局函数容易引起污染全局使用域
【代码示例】
void Function1(); // bad
namespace myproject {
void Function2(); // good
}
2.3.2【规范】将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果
【说明】
类的静态方法应当和类的实例或静态数据紧密相关
【代码示例】
选择
namespace myproject {
namespace foo_bar { // 优先使用命名空间
void Function1();
void Function2();
} // namespace foo_bar
} // namespace myproject
而非
namespace myproject {
class FooBar { // FooBar 只有两个静态成员方法,与类并不强相关
public:
static void Function1();
static void Function2();
};
} // namespace myproject
2.4 局部变量
2.4.1【规范】将函数变量尽可能置于最小作用域内
【说明】
方便读者更快的找到相关的定义,并在变量声明时进行初始化
【代码示例】
while,for循环中使用的局部变量应该直接定义在声明(statement)里,如下例:
while (const char* p = strchr(str, '/'))
str = p + 1;
2.4.2【建议】如果在循环中的局部变量是一个类类型,那么把这个对象移到循环外会更加高效
【说明】
放在循环外,可以减少构造函数和析构函数的调用次数
【代码示例】
选择
Foo f; // 构造函数和析构函数只调用 1 次
for (int i = 0; i < 1000000; ++i) {
f.DoSomething(i);
}
而非
// 低效的实现
for (int i = 0; i < 1000000; ++i) {
Foo f; // 构造函数和析构函数分别调用 1000000 次!
f.DoSomething(i);
}
2.5 静态和全局变量
静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量。
原生数据类型(POD : Plain Old Data): 即 int, char 和 float, 以及 POD 类型的指针、数组、结构体。
2.5.1【建议】不要 定义静态储存周期的 非 trivial析构的 类对象
【说明】
C++静态存储周期定义对象的析构销毁顺序是未定义的,特别是在多线程环境,多dll环境下,特别复杂。因此,如果析构函数是非trival的,那么就有可能会面临在析构函数中去访问一个已经在其它线程/编译单元 中释放的对象或者资源 的风险。(同一个编译单元内是明确的,静态初始化优先于动态初始化,初始化顺序按照声明顺序进行,销毁则逆序。不同的编译单元之间初始化和销毁顺序属于未明确行为)。
因此,定义成静态存储周期的对象变量的析构函数不应该做任何显式操作(trival),只允许trivial 析构型的 静态存储持续时间对象。基本类型(如指针和int)是trivial 析构的,trivial 析构类型的数组也是trivial 析构的。
如果用户不定义析构函数,而是用系统自带的,则说明,析构函数基本没有什么用(但默认会被调用)我们称之为trivial destructor。反之,如果特定定义了析构函数(例如需要释放内存,关闭打开的文件描述符等操作),则说明需要在释放空间之前做一些事情,则这个析构函数称为non-trivial destructor。
如果全局变量的声明(单独考虑)可以是constexpr,那么它就满足这些要求。
【代码示例】
const int kNum = 10; // allowed
struct X { int n; };
const X kX[] = {{1}, {2}, {3}}; // allowed
void foo() {
static const char* const kMessages[] = {"hello", "world"}; // allowed
}
// allowed: constexpr guarantees trivial destructor
constexpr std::array<int, 3> kArray = {{1, 2, 3}};
// bad: non-trivial destructor
const std::string kFoo = "foo";
// bad for the same reason, even though kBar is a reference (the
// rule also applies to lifetime-extended temporary objects)
const std::string& kBar = StrCat("a", "b", "c");
void bar() {
// bad: non-trivial destructor
static std::map<int, int> kData = {{1, 0}, {2, 0}, {3, 0}};
}
2.5.2【规范】禁止定义静态储存周期非POD变量
【说明】
原因同 2.5.1;
2.5.3【规范】禁止使用含有副作用的函数初始化POD全局变量
【说明】
多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植
2.5.4【建议】函数静态局部变量可以使用动态初始化,但不鼓励对静态类成员变量或命名空间范围内的变量使用动态初始化
2.5.5 【建议】一些比较好的实践方法
- 对于全局使用的字符串,使用
char
数组或者char *
- 如果你需要一个静态的、固定的容器实例,比如一个查找表,不要使用
map
,set
或者其他动态容器。尝试使用std::array
或者std::array<std::pair<>>
,对于数据较小的情况,线性查找也够用。尽量保证数据经过了排序,并使用二分查找。 - 如果必须使用动态容器,you can create an object dynamically and never delete it by using a function-local static pointer or reference (e.g., static const auto& impl = *new T(args...);).
2.6 threadlocal Variables
【说明】
thread_local
对象针对每个线程都有一个实例,可以定义成thread_local的变量有:
- 命名空间下的全局变量;
- 类的
static
成员变量; - 本地变量;
优点
- 避免资源竞争;
缺点
- 访问
thread_local
变量可能会触发不可预测且无法控制的其他代码的执行; - thread_local variables are effectively global variables, and have all the drawbacks of global variables other than lack of thread-safety. 如果类的成员函数内定义了
thread_local
变量,则对于同一个线程内的该类的多个对象都会共享一个变量实例,并且只会在第一次执行这个成员函数时初始化这个变量实例,这一点是跟类的静态成员变量类似的; - 占用的内存随线程数量的增加而增加;
- 普通类成员无法定义成
thread local
;
2.6.1【建议】函数中的thread_local变量没有安全问题,因此可以不受限制地使用它们
【说明】
请注意,您可以使用函数作用域中的 thread_local
变量来模拟类或命名空间作用域中的 thread_local
变量,方法是定义公开该 thread_local
的函数或静态方法。
【代码示例】
Foo& MyThreadLocalFoo() {
thread_local Foo result = ComplicatedInitialization();
return result;
}
2.6.2【建议】类或名称空间范围内的thread_local
变量必须使用编译时常量进行初始化(即,它们必须没有动态初始化)
【说明】
为了实现这一点,类或命名空间范围内的thread_local变量必须用ABSL_CONST_INIT(或constexpr,但这应该很少)进行注释。
2.6.3【建议】如果有其他方案可以实现thread_local相同的功能,应当首先考虑其他方案
3 类
3.1 构造函数的职责
3.1.1【规范】不要在构造函数中调用虚函数,也不要在无法报出错误时进行可能失败的初始化
【说明】
构造函数不应该调用虚函数。如果适用于您的代码,终止程序可能是一种适当的错误处理响应。否则,考虑一个工厂函数或 Init()
方法。
3.1.2【建议】对于缺省实现的构造函数(同理赋值操作符 或析构函数),使用 =default
或者 =delete
显式说明
【说明】
=default
显式的要求编译器生成函数的一个默认版本,对于构造函数而言(尤其是拷贝/移动构造函数中),可以减轻编码负担。
除此之外,相比于什么都不写,可以起到显示指定构造函数的权限作用,即编译器默认生成的构造函数都是 public
权限,而 使用 =default
可以显示指定权限。
而相比写一个空的,需要知道 一旦某个类有了一个用户定义的构造函数,那这个类就不再是 aggregate 类型,和不再是 trivial 类型,和不再是 POD 类型了。
【代码示例】
#include <type_traits>
struct X { X() = default; };
struct Y { Y() {}; };
int main() {
static_assert(std::is_trivial<X>::value, "X should be trivial");
static_assert(std::is_pod<X>::value, "X should be POD");
static_assert(!std::is_trivial<Y>::value, "Y should not be trivial");
static_assert(!std::is_pod<Y>::value, "Y should not be POD");
}
3.2 隐式类型转换
3.2.1【规范】不要定义隐式类型转换,对于转换运算符和单参数构造函数,请使用 explicit
关键字.
【说明】
不要提供隐式类型转换,使用 ToFloat()
等函数代替。
隐式类型转换允许一个某种类型 (称作 源类型) 的对象被用于需要另一种类型 (称作 目的类型) 的位置。例如将一个 int
类型的参数传递给需要 double
类型的函数。
除了语言所定义的隐式类型转换,用户还可以通过在类定义中添加合适的成员定义自己需要的转换。在源类型中定义隐式类型转换,可以通过目的类型名的类型转换运算符实现(例如 operator bool()
)。在目的类型中定义隐式类型转换,则通过以源类型作为其唯一参数(或唯一无默认值的参数)的构造函数实现,例如在判断指针是否会空时的示例。
std::shared_ptr<int> ptr = func();
if (ptr) {
// do sth
}
优点
- 有时目的类型名是一目了然的,通过避免显式地写出类型名,隐式类型转换可以让一个类型的可用性和表达性更强。
- 隐式类型转换可以简单地取代函数重载。
- 在初始化对象时, 列表初始化语法是一种简洁明了的写法。
缺点 - 隐式类型转换会隐藏类型不匹配的错误。有时,目的类型并不符合用户的期望,甚至用户根本没有意识到发生了类型转换。
- 隐式类型转换会让代码难以阅读, 尤其是在有函数重载的时候, 因为这时很难判断到底是哪个函数被调用。
- 单参数构造函数有可能会被无意地用作隐式类型转换。
- 如果单参数构造函数没有加上 explicit 关键字, 读者无法判断这一函数究竟是要作为隐式类型转换, 还是作者忘了加上 explicit 标记。
- 并没有明确的方法用来判断哪个类应该提供类型转换, 这会使得代码变得含糊不清。
- 如果目的类型是隐式指定的, 那么列表初始化会出现和隐式类型转换一样的问题, 尤其是在列表中只有一个元素的时候。
【代码示例】
示例如下,当然如果程序逻辑本意是想通过A对象构造一个B对象去给 f 用,那么我们应该拒绝这种本意,因为给阅读代码的人会带来太多的困扰。
// A simple class
class A {};
// Another simple class with a single-argument constructor for class A
class B
{
public:
B() {}
B(A const&) {}
};
// A function that expects a 'B'
void f(B const&) {}
int main()
{
A obj;
f(obj); // Spot the deliberate mistake
}
3.3 拷贝与移动
3.3.1【规范】如果你的类型需要拷贝 / 移动,就显式的写出来(自行实现或者显式使用=default),否则, 就显式地把隐式产生的拷贝和移动函数禁用
【说明】
见 3.1.2
【代码示例】
3.3.2【规范】如果显式定义了拷贝/移动构造函数,那也需要同时定义相应的拷贝/移动操作符,反之亦然
【说明】
这些声明/删除只有在很明显的情况下才能省略:
- 类没有private段,比如纯数据结构体或者接口类。
- 基类显式禁止了拷贝 / 移动(=delete)。子类必然不会自动生成。对于接口类也不需要显式禁用。
只在移动语义的性能明显优于拷贝时才需要显式定义。
【代码示例】
class rule_of_five{
char* cstring; // raw pointer used as a handle to a dynamically-allocated memory block
public:
rule_of_five(const char* s = "")
: cstring(nullptr)
{
if (s) {
std::size_t n = std::strlen(s) + 1;
cstring = new char[n]; // allocate
std::memcpy(cstring, s, n); // populate
}
}
~rule_of_five()
{
delete[] cstring; // deallocate
}
rule_of_five(const rule_of_five& other) // copy constructor
: rule_of_five(other.cstring)
{}
rule_of_five(rule_of_five&& other) noexcept // move constructor
: cstring(std::exchange(other.cstring, nullptr))
{}
rule_of_five& operator=(const rule_of_five& other) // copy assignment
{
return *this = rule_of_five(other);
}
rule_of_five& operator=(rule_of_five&& other) noexcept // move assignment
{
std::swap(cstring, other.cstring);
return *this;
}
// alternatively, replace both assignment operators with
// rule_of_five& operator=(rule_of_five other) noexcept
// {
// std::swap(cstring, other.cstring);
// return *this;
// }
};
3.3.3【建议】为了防止出现切片,应该避免为被设计作为基类的类提供公共赋值运算符或复制/移动构造函数,如果基类需要可复制,请提供一个公共的virtual Clone()
方法和一个受保护的复制构造函数,派生类可以使用该构造函数来实现它
【说明】
【代码示例】
如下,d2 对象的构造过程中,只调用到了派生类的拷贝构造函数, 并没有调用到 基类的拷贝构造函数。
class Base {
public:
Base() { std::cout << "Base Default Constructor" << std::endl; }
Base(const Base &) { std::cout << "Base Copy Constructor" << std::endl; }
};
class Drived : public Base {
public:
Drived() {
std::cout << "Drived Default Constructor" << std::endl; }
Drived(const Drived& d) {
std::cout << "Drived Copy Constructor" << std::endl;
}
};
int main(void) {
Drived d1; // 输出 :Base Default Constructor
// Drived Default Constructor
Drived d2(d1); // 输出 : Base Default Constructor // 调用了基类的默认构造函数而不是拷贝构造
// Drived Copy Constructor
}
一个简单的解决办法如下,这本身并不难,但是可能会造成非常难定位的Bug,因此十分建议,禁用基类的拷贝构造或移动构造函数。
Drived(const Drived& d) : Base(d) {
cout << "Drived Copy Constructor" << endl;
}
如果基类的拷贝难以避免时,也非常建议使用 public virtual clone 方法 应付多态的使用场景。
class B {
public:
virtual B* clone() = 0;
B() = default;
virtual ~B() = default;
B(const B&) = delete;
B& operator=(const B&) = delete;
};
class D : public B {
public:
D* clone() override;
~D() override;
};
3.4 结构体 和 类
3.4.1【规范】仅当只有数据成员(POD类型)时使用 struct, 其它一概使用 class
3.4.2【建议】结构体不应有private成员变量,变量间不应有隐含的关联,否则用户直接访问这些变量将会破坏这种关联
3.4.2【建议】在模板编程中,对于无状态类型,例如traits、模板元函数和某些functor,可以使用struct而不是class
3.5 Structs Pairs Tuples
3.5.1【建议】尽可能的使用struct而不是pair和tuple
【说明】
这样每个元素都可以有自己的名字。虽然使用 pair
和 tuple
可以避免定义自定义类型,在编写代码时可能会节省工作,但在读取代码时,有意义的字段名总是比 .first
、.second
或 std::get<X>
更清晰。
3.6 继承
3.6.1【建议】使用组合常常比使用继承更合理。如果使用继承的话,定义为 public 继承
【说明】
如果场景确实是需要使用 Private
或者 Protect
继承,那么就请优先考虑使用组合的方式替代(即把基类的实例作为成员对象的方式)。
【代码示例】
class Fly {
};
class Animal {
};
class Bird : public Animal {
private:
Fly flyable_; // 组合
}
3.6.2【建议】只有在严格满足"is-a"的情况下使用继承
【说明】
继承作为面向对象四大特性之一,可以有效解决代码复用问题,同时也是多态的实现基础。但是如果过于复杂的继承关系,一方面影响到代码可读性,例如要查阅类的某个方法时,可能需要查阅父类的代码,父类父类的代码,一直到最顶层父类的代码。另一方面,可维护性变差,父类的实现细节曝露给子类,父类的任何改动都会影响到子类。
如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。除此之外,还有一些设计模式会固定使用继承或者组合。比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。
参见 Effective C++ item 32: Make Sure Public Inheritance Models "is-a"。
3.6.3【规范】允许多重继承,但强烈反对多重 实现继承
【说明】
继承分两类:
- 接口继承(Interface inheritance):指继承自纯虚基类(没有状态以及定义函数,纯定义接口)
- 实现继承(implementation inheritance):除接口继承外都是实现继承
关于多重继承的副作用可参见 3.7.1
3.6.4【规范】对显式重写的虚函数要使用 override
修饰,重写虚函数时不要添加 virtual
关键字
【说明】
标记为 override
或 final
的析构函数,如果不是对基类虚函数的重载的话,编译会报错,让错误曝露在编译阶段总是更好的。
例如,你可能 目的是需要重写基类中的一个虚函数时,但由于笔误,把参数从int32_t
写成了 int64_t
,甚至是函数名少写了一个字母。此时如果没有 override 关键字修饰的话,编译器不会报错,逻辑就变成了重载,错过一次修正代码的绝佳机会。
【代码示例】
class Base {
public:
virtual void do_something() {}
};
class Derived :public Base {
public:
void do_something() {} // bad: miss override
void do_something() override {} // good
virtual void do_something() override {} // bad: remove virtual
};
3.7 多重继承
3.7.1【建议】只在以下情况我们才允许多重继承:最多只有一个基类是非抽象类; 其它基类都是以 Interface 为后缀的纯接口类
【说明】
多重继承,虽然相比于单继承能复用更多的代码,但是带来的副作用也是比较明显的。
一个非常显著的麻烦就是 二义性。例如两个基类中含有同名方法时,那么在派生类调用过程中必须指名是来自哪个基类。更糟糕的情况是,如果两个基类又都派生自一个共同的父类,这样构造了菱形继承的模型。例如 D 继承自 B 和 C ,而B C 又都继承自A,如此A类中所有的成员都在D类中都产生了二义性。虽然可以使用虚继承解决,但是前提是你对虚继承的副作用有足够的了解(更复杂的父子类转换关系,以及更复杂的内存结构)。
3.7.2【建议】对于多重继承,建议把stateless class写在继承体系前面,利用ECO(Empty Class Optimiaztion)优化内存占用
【说明】
【代码示例】
class Empty{
public:
void print() {
std::cout<<"I am Empty class"<<std::endl;
}
};
class notEbo {
int i;
Empty e;
// do other things
};
class ebo: public Empty {
int i;
// do other things
};
std::cout<<sizeof(notEbo)<<std::endl; // 8 内存对齐
std::cout<<sizeof(ebo)<<std::endl; // 4
3.8 运算符重载
【说明】
C++ 允许用户通过使用 operator
关键字 对内建运算符进行重载定义,只要其中一个参数是用户定义的类型。operator
关键字还允许用户使用 operator""
定义新的字面运算符,并且定义类型转换函数,例如 operator bool()
。
优点
- 重载运算符可以让代码更简洁易懂,也使得用户定义的类型和内建类型拥有相似的行为。重载运算符对于某些运算来说是符合符合语言习惯的名称 (例如 ==, <, =, <<),遵循这些语言约定可以让用户定义的类型更易读,也能更好地和需要这些重载运算符的函数库进行交互操作。
- 对于创建用户定义的类型的对象来说,用户定义字面量是一种非常简洁的标记。
缺点 - 要提供正确、一致,不出现异常行为的操作符运算需要花费不少精力,而且如果达不到这些要求的话,会导致令人迷惑的 Bug。
- 过度使用运算符会带来难以理解的代码,尤其是在重载的操作符的语义与通常的约定不符合时。
- 函数重载有多少弊端,运算符重载就至少有多少。
- 运算符重载会混淆视听,让你误以为一些耗时的操作和操作内建类型一样轻巧。
- 对重载运算符的调用点的查找需要的可就不仅仅是像 grep 那样的程序了,这时需要能够理解 C++ 语法的搜索工具。
- 如果重载运算符的参数写错,此时得到的可能是一个完全不同的重载而非编译错误。例如: foo < bar 执行的是一个行为, 而 &foo < &bar 执行的就是完全不同的另一个行为了。
- 重载某些运算符本身就是有害的。例如,重载一元运算符 & 会导致同样的代码有完全不同的含义, 这取决于重载的声明对某段代码而言是否是可见的。重载诸如 &&, || 和 , 会导致运算顺序和内建运算的顺序不一致。
- 运算符从通常定义在类的外部,所以对于同一运算,可能出现不同的文件引入了不同的定义的风险。 如果两种定义都链接到同一二进制文件,就会导致未定义的行为,有可能表现为难以发现的运行时错误。
- 用户定义字面量所创建的语义形式对于某些有经验的 C++ 程序员来说都是很陌生的。
3.8.1【建议】除少数特定环境外, 不要重载运算符。也不要创建用户定义字面量(operator"")
【说明】
不要刻意避免定义运算符重载。例如,更倾向于定义==、=、<<,而不是定义Equals()、CopyFrom()和PrintTo()。相反,不要仅仅因为其他库需要运算符重载就定义它们。例如,如果你的类型没有自然排序,但希望将其存储在std::set中,请使用自定义比较器,而不是重载<操作符。
3.8.2【规范】不要重载&、||、,或一元&或""也就是说, 不要引入用户定义字面量
【说明】
C11新标准中引入了用户自定义字面量,也叫自定义后缀操作符,即通过实现一个后缀操作符,将申明了该后缀标识的字面量转化为需要的类型。但是用户定义字面量所创建的语义形式对于某些有经验的 C 程序员来说都是很陌生的。
【代码示例】
long double operator"" _mm(long double x) { return x / 1000; }
long double operator"" _m(long double x) { return x; }
long double operator"" _km(long double x) { return x * 1000; }
int main()
{
cout << 1.0_mm << endl; //0.001
cout << 1.0_m << endl; //1
cout << 1.0_km << endl; //1000
return 0;
}
3.8.3【规范】仅当重载运算符的含义显而易见且与相应的内置运算符一致时,才定义重载运算符
【说明】
例如,将|用作按位或逻辑or,而不是shell样式的管道,即不能与内置运算符的语义违背。
3.9 存取控制
3.9.1【建议】将所有数据成员声明为 private,除非是 static const 类型成员
【说明】
如果让一个数据成员为 public
,每一个人都可以读写访问它,但是如果使用函数去得到和设置它的值,就能实现禁止访问,只读访问和读写访问等控制。
对于 message 较为难以遵循,可以考虑用 mutable_xxx()
和 xxx()
进行区分或set_xxx()
和 xxx()
两种形式,message中存在较多 map
和 vector
变量,可以考虑前者。针对单类型变量可以考虑后者。
【代码示例】
class Point {
public:
Point(int x, int y) : x_(x), y_(y) {}
int x() { return x_; }
int y() { return y_; }
int set_x(int x) { x_ = x;}
int set_y(int y) { y_ = y;}
private:
int x_;
int y_;
};
int main() {
Point p(3, 4);
std::cout << p.x() << "," << p.y() << std::endl;
}
3.10 声明顺序
3.10.1【规范】将相似的声明放在一起, 将 public 部分放在最前,后跟protected:,然后是private:。省略为空的部分,且通过空行将不同组的声明隔开
【说明】
一个约定的习惯而已。public 通常用于修饰函数成员放在最前面,方便阅读。其次,protected一般用来继承,所以放在中间,这也是用户所关心的。最后,private是保护数据成员的,一般不用用户来关心,所以一般放在最后。
3.10.2【建议】每个部分中,建议按以以下顺序定义:类型(包括typedef、using、enum和嵌套结构和类)、常量、工厂函数、构造函数和赋值运算符、析构函数、所有其他方法、数据成员
4 函数
4.1 参数顺序
4.1.1【规范】函数的参数顺序为:输入参数在先,后接输入输出参数,最后接输出参数(新增加的参数也需要满足规则)
【代码示例】
// Parameter order: input -> input/output -> output
// Compliant - input string and output parsed config if success
bool ParsingConfigFrom(const std::string& str, Config& config);
// Compliant - input start offset, and output end offset
bool ParsingConfigFrom(const std::string& str, size_t& offset, Config& config);
// Non-compliant - output first, miss ordered
bool ParsingConfigFrom(Config& config, const std::string& str);
// Also compliant - trailing default parameter
bool ParsingConfigFrom(const std::string& str, Config& config, char delim=',');
4.1.2【建议】函数参数不超过5个
【说明】
可能有两种情况导致参数过多
- 函数功能过于庞大,这种情况考虑拆成多个小函数,每一个都包含一小部分参数,这样做有2个好处
- 提高了代码可读性
- 让单元测试更容易
- 可能隐藏了一个潜在的类,这个类包含了这些的参数,可以参考 Parameter Object Pattern
4.1.3【建议】C++函数的输出一般通过返回值提供,有时通过输出参数(或输入/输出参数)提供
【说明】
相比于输出参数,更倾向于使用返回值:它们提高了可读性,并且通常提供相同或更好的性能。更倾向于按值返回,否则,通过引用返回。避免返回指针,除非它可以为null。
【代码示例】
// Not recommand - if fails, we still need construct pizza
bool MakePizza(Pizza& pizza) {
if (!busy) {
pizza.size = 6;
pizza.flavor = "Spicy";
return true;
} else {
return false;
}
}
// Better, no constructor when failed, but heap allocation if success
std::unique_ptr<Pizza> MakePizza();
// Recommand - if failed, no constructor and heap allocation overhead
std::optional<Pizza> MakePizza() {
if (!busy) {
return Pizza {6, "Spicy"};
} else {
return {}
}
}
4.1.4【建议】仅作为输入的参数通常应该是值或常量引用,仅作为输出或输入/输出的参数通常应该是引用(不能为null)
【说明】
通常,使用 std::optional(since c++17)
或者 boost::optional
表示可选的按值输入,或者使用常量指针。使用非常量指针表示可选输出和可选输入/输出参数。(可选是指输入输出参数非必需,为null时表示没有对应输入、输出)。
【代码示例】
// Small object(int like), pass by value
int Process(int i)
// Normal/Big object, pass by reference
int Process(const std::string& s)
// Exception in setter/getter/constructor, if removable, pass by value
//
void Person:: Person(std::string name) :
name_(std::move(name)) {
}
### 4.1.5【建议】避免定义生命周期长于函数调用的常量引用参数
【说明】
因为常量引用参数可以绑定临时变量。相反,找到一种消除生存期需求的方法(例如,通过复制参数),或者通过const指针传递它。
【代码示例】
// Don't do this
class StringHolder {
public:
// The input `val` must live as long as this object, not just the call to
// this c'tor
StringHolder(const string& val) : val_(val) {}
const string& get() { return val_; }
private:
const string& val_;
};
StringHolder holder("abc"s);
std::cout << holder.get(); // boom, UB. The string temporary has already
// been destroyed, the reference is dangling.
4.2 编写简短函数
倾向于编写简短, 凝练的函数。
4.2.1【建议】在编写函数时,需要综合考虑函数的 圈复杂度、可执行行数、函数调用数、嵌套深度等指标
【说明】
长函数有时是合适的,所以对函数的长度没有硬性限制。如果一个函数超过大约70行,请考虑是否可以在不损害程序结构的情况下分解它。
即使你的长函数现在运行得很好,几个月后有人修改它,可能会增加新的行为。这可能会导致很难找到的bug。保持你的函数简短和简单可以让其他人更容易阅读和修改您的代码。小功能也更容易测试。
在使用某些代码时,可能会发现长而复杂的函数。不要被修改现有代码所吓唬:如果使用这样的函数证明是困难的,你会发现错误很难调试,或者你想在几个不同的上下文中使用它,考虑把函数分解成更小的和更易管理的块。
一个可供参考的意见,在功能安全里的定义如下
| Item | Desc | Criteria |
| ------ | ------ | ------ |
| STCDN | 注释与代码比率 | > 0.2 |
| STCYC | 圈复杂度 | < 15 |
| STXLN | 可执行行数 | < 70 |
| STSUB | 函数调用数 | < 10 |
| STMIF | 嵌套深度 | < 5 |
| STPTH | 估计静态路径数 | < 250 |
4.3 引用参数
4.3.1【规范】所有按引用传递的输入参数(只读属性)必须加上 const
【说明】
对于不会被修改的参数,必须使用const进行修饰,从而保证函数声明的准确性。同时,项目中的存取控制函数也没有提供const的版本,导致其他模块只需要进行读取也不能加上const
修饰。
【代码示例】
// Prefer const Circle& than Circle&, if DrawCircle doesn't change circle
void DrawCircle(const Circle& circle);
4.4 函数重载
4.4.1【建议】若要使用函数重载, 则必须能让读者一看调用点就胸有成竹, 而不用花心思猜测调用的重载函数到底是哪一种. 这一规则也适用于构造函数
【代码示例】
#include<iostream>
using namespace std;
void function(int x)
{
std::cout << "Value of x is : " << x << std::endl;
}
void function(int y, int z=1)
{
std::cout << "Value of y is : " << y << std::endl;
}
int main()
{
function(34); // ambigous
return 0;
}
4.5 缺省参数
4.5.1【规范】只允许在非虚函数中使用缺省参数(缺省参数是编译期绑定的),且必须保证缺省参数的值始终一致。缺省参数与 函数重载 遵循同样的规则。 一般情况下建议使用函数重载
【代码示例】
// Don't use default parameter on virtual function
class Foo
{
public:
virtual void doIt(int x = 1)
{
std::cout << "Doing foo: " << x << std::endl;
}
};
class Bar : public Foo
{
public:
virtual void doIt(int x = 0)
{
std::cout << "Doing bar " << x << std::endl;
}
};
int main() {
Bar bar;
Foo* fooPtr = &bar;
fooPtr->doIt(); // Doing bar: 1
// we got the right function, but the wrong default value
}
4.6 函数返回类型后置语法
4.6.1【建议】只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法
4.7 goto
4.7.1【建议】尽量避免使用goto
【说明】
goto statement 会破坏程序的结构,增加debug的难度。
【代码示例】
void f1 (int a) {
if (a <=0) {
goto L2; // jumps into a different block
}
if (a == 0) {
{
goto L1;
}
goto L2; // jumps into a block
L1:
for (int i = 0; i < a; i++) {
L2:
//... Should only have come here with a >=0. Loop is infinite if a < 0
}
}
5 来自Google的奇技
5.1 所有权与智能指针
5.1.1【建议】动态分配出的对象最好有单一且固定的所有主, 并通过智能指针传递所有权,优先考虑使用 unique_ptr
【代码示例】
// perfer owning pointer over raw in virtual c'tor function
class Investment {...};
class Stock: public Investment {...};
class Bond: public Investment {...};
class RealEstate: public Investment {...};
template <typename... Ts>
std::unique_ptr<Investment> makeInvestment(Ts&&... params);
auto pInvestment = makeInvestment(args);
5.1.2【建议】优先用std::make_unique和std::make_shared而不是直接new (来自 effective C++)
【代码示例】
// 1. duplicated type Widget
std::unique_ptr<Widget> upw(new Widget);
// 2. potential resource leak! if computePriority throw exception
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
// 3. efficency, using one malloc totally
auto spw(std::make_shared< Widget >());
5.2 Cpplint
5.2.1【规范】使用 cpplint.py 检查风格错误.
6 其他 C++ 特性
6.1 右值引用
6.1.1【规范】仅在如下情况下使用右值引用
- 定义移动构造函数与移动赋值操作时使用右值引用。
- 完美转发。之前的版本强调不要使用 std::forward ,最新版本已去除此项限制
- 可以使用它们来定义重载对,例如一个采用Foo&&而另一个采用const Foo&。通常,首选的解决方案是按值传递,但重载的函数对有时会产生更好的性能,并且有时在需要支持多种类型的通用代码中是必需的。一如既往:如果您为了性能而编写更复杂的代码,请确保您有证据证明它确实有帮助。
【代码示例】
// r value in move c'tor
// Move constructor.
struct MemoryBlock {
MemoryBlock(MemoryBlock&& other) noexcept;
MemoryBlock& operator=(MemoryBlock&& other) noexcept;
}
// r value in perfect forwarding
template <typename T>
void print(T&& obj) {
print_impl(std::forward<T>(obj));
}
// define both L & R value version when performance maters
template <typename T>
void array<T>::push_back(const T& someth);
template <typename T>
void array<T>::push_back(T&& someth);
6.2 友元
6.2.1【建议】建议合理的使用友元类及友元函数
【说明】
友元通常应该在同一个文件中定义,这样读者就不必在另一个文件中查找类的私有成员的用法。
友元扩展但不打破类的封装边界。在某些情况下,这比只想给一个其他类访问成员却将其定义成public要好。但是,大多数类应该仅通过其公共成员与其他类交互。
6.3 异常
6.3.1【建议】不使用 C++ 异常
【说明】
据说使用异常会对程序性能造成影响,但以代码可读性的角度来看的话,返回错误码真的比抛出异常要好吗?拓展阅读: 对使用 C++ 异常处理应具有怎样的态度?
6.4 noexcept
6.4.1【建议】当 noexcept
有用且正确时,指定 noexcept
【说明】
noexcept
说明符用于指定函数是否抛出异常。如果从标记为 noexcept
的函数中抛出了异常,则程序将通过 std::terminate
崩溃。
noexcept
运算符执行编译时检查,如果声明表达式不引发任何异常,则返回true
。
如果noexcept准确地反映了函数的预期语义(即,如果异常以某种方式从函数体中抛出,则表示致命错误),则可以在对性能有用时使用noexcept。你可以认为将移动构造函数声明成noexcept可以提升性能。如果认为在某些其他功能上指定noexcept有显著的性能优势,请与你的项目负责人讨论。
【代码示例】
struct MemoryBlock {
// move construct without noexcept
MemoryBlock(MemoryBlock&& other);
...
}
std::vector<MemoryBlock> blocks;
MemoryBlock a_block{1024};
blocks.push_back(std::move(a_block)); // here copy construct is called instead of move
// because move c'tor is NOT noexcept
6.5 运行时类型识别
6.5.1【建议】尽量不使用 RTTI,除了hobotsdk message等必须使用的情况
【说明】
dynamic_cast
最坏的情况下比 reinterpret_cast
版本慢了 16 倍,同时每个对象会增加 typeinfo
的空间。
g++ 4.4.1 | Ticks | Relative Factor |
---|---|---|
dynamic_cast level1-to-base success | 6008176 | 1.00 |
dynamic_cast level2-to-base success | 7009504 | 1.17 |
dynamic_cast level2-to-level1 success | 36055776 | 6.00 |
dynamic_cast level3-to-base success | 6008176 | 1.00 |
dynamic_cast level3-to-level1 success | 43178320 | 7.19 |
dynamic_cast level3-to-level2 success | 36056256 | 6.00 |
dynamic_cast onebase-to-twobase fail | 27042336 | 4.50 |
dynamic_cast onelevel1-to-twobase fail | 33051232 | 5.50 |
dynamic_cast onelevel2-to-twobase fail | 39096128 | 6.51 |
dynamic_cast onelevel3-to-twobase fail | 99445824 | 16.55 |
dynamic_cast same-type-base success | 7009392 | 1.17 |
dynamic_cast same-type-level1 success | 30931008 | 5.15 |
dynamic_cast same-type-level2 success | 30442688 | 5.07 |
dynamic_cast same-type-level3 success | 30478432 | 5.07 |
member variable + reinterpret_cast | 8013216 | 1.33 |
reinterpret_cast known-type | 6008160 | 1.00 |
virtual function + reinterpret_cast | 11017248 | 1.83 |
在运行时查询对象的类型通常意味着设计问题。在运行时需要知道对象的类型通常表明类层次结构的设计有缺陷。
RTTI有合理的用途,但容易被滥用,所以在使用时必须小心。可以在单元测试中自由使用它,但在其他代码中尽可能避免使用它。特别是,在新代码中使用RTTI之前要三思。如果发现自己需要编写基于对象类的行为不同的代码,请考虑下列选项之一来查询类型:
- 虚拟方法是根据特定子类类型执行不同代码的首选方法。这将工作放在对象本身内。
- 如果工作属于对象之外,而不是在某些处理代码中,则考虑double-dispatch解决方案,例如访问者(Visitor)设计模式。
【代码示例】
当程序的逻辑保证一个基类的实例实际上是一个特定派生类的实例时,可以在对象上自由使用 dynamic_cast
。在这种情况下,通常可以使用static_cast
作为替代。
/* -------------------------------- Added Visitor Classes ------------------------------- */
struct AnimalVisitor {
virtual void Visit(struct Cat *) = 0;
virtual void Visit(struct Dog *) = 0;
};
struct ReactVisitor : AnimalVisitor {
ReactVisitor(struct Person *p) : person{p} {}
void Visit(struct Cat *c);
void Visit(struct Dog *d);
struct Person *person = nullptr;
};
/* --------------------------------------------------------------------------------------- */
struct Animal {
virtual string name() = 0;
virtual void Visit(struct AnimalVisitor *visitor) = 0;
};
struct Cat : Animal {
string name() { return "Cat"; }
void Visit(AnimalVisitor *visitor) { visitor->Visit(this); } // 2nd dispatch <<---------
};
struct Dog : Animal {
string name() { return "Dog"; }
void Visit(AnimalVisitor *visitor) { visitor->Visit(this); } // 2nd dispatch <<---------
};
struct Person {
void ReactTo(Animal *_animal) {
ReactVisitor visitor{this};
_animal->Visit(&visitor); // 1st dispatch <<---------
}
void RunAwayFrom(Animal *_animal) { cout << "Run Away From " << _animal->name() << endl; }
void TryToPet(Animal *_animal) { cout << "Try To Pet " << _animal->name() << endl; }
};
/* -------------------------------- Added Visitor Methods ------------------------------- */
void ReactVisitor::Visit(Cat *c) { // Finally comes here <<-------------
person->TryToPet(c);
}
void ReactVisitor::Visit(Dog *d) { // Finally comes here <<-------------
person->RunAwayFrom(d);
}
/* --------------------------------------------------------------------------------------- */
int main() {
Person p;
for(auto&& animal : vector<Animal*>{new Dog, new Cat})
p.ReactTo(animal);
return 0;
}
6.6 类型转换
6.6.1【规范】使用 C++ 的类型转换, 如 static_cast<>(). 不要使用 int y = (int)x 或 int y = int(x) 等转换方式
【说明】
- 用
static_cast
替代 C 风格的值转换, 或某个类指针需要明确的向上转换为父类指针时。 - 用
const_cast
去掉const
限定符。 - 用
reinterpret_cast
指针类型和整型或其它指针之间进行不安全的相互转换. 仅在你对所做一切了然于心时使用。
6.7 流
6.7.1【建议】只在记录日志时使用流
6.8 前置自增和自减
6.8.1【建议】对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符
【代码示例】
std::vector v {1, 2, 3};
// perfer ++iter over iter++
for (auto& iter = v.begin(); iter != v.end() ; ++iter) {
...
}
// perfer range-based loop over the above one
for (auto elem : v) { // auto deduced to int
...
}
6.9 const 用法
6.9.1【建议】我们强烈建议你在任何可能的情况下都要使用 const. 此外有时改用 C++11 推出的 constexpr 更好
【说明】
强烈建议在 API中使用 const(即函数参数、方法和非局部变量),只要它是有意义和准确的。有一种一致可靠的方法来区分读和写对于编写线程安全代码至关重要,并且在许多其他上下文中也很有用。尤其是:
- 如果函数保证不会修改通过引用或指针传递的参数,则相应的函数参数应分别是对const(const T&)的引用或对const(const T*)的指针。
- 对于通过值传递的函数参数,const对调用方没有影响,因此不建议在函数声明中使用。
- 将方法声明为常量,除非它们改变对象的逻辑状态(或允许用户修改该状态,例如通过返回非常量引用,但这很少见),或者不能安全地同时调用它们。(返回非const引用或指针的函数不要声明成const)
- 类的所有const操作都应该可以安全地相互并发调用。如果不可行,则必须将该类明确记录为“线程不安全”。
【代码示例】
// various versions of const are explained below
#include <iostream>
class Entity {
public:
int GetX() const // can't modify class variables
{
// x_ = 4; // error! private member can't be modified inside const method
return x_;
}
int Get_X() // will modify class
{
x_ = 4; // ok
return x_;
}
void PrintEntity(const Entity& e) { // const type
// e.Get_X() // error! we cannot access to non-const member function
std::cout << e.GetX() << std::endl;
}
private:
int x_, y_;
};
6.9.2【建议】const的位置,推荐使用const XXX &类型的格式
6.10 constexpr 用法
6.10.1【建议】在 C++11 里,用 constexpr 来定义真正的常量,或实现常量初始化
【说明】
constexpr definitions enable a more robust specification of the constant parts of an interface. Use constexpr to specify true constants and the functions that support their definitions. Avoid complexifying function definitions to enable their use with constexpr. Do not use constexpr to force inlining.
https://en.cppreference.com/w/cpp/language/constexpr
【代码示例】
#include <iostream>
#include <stdexcept>
// C++11 constexpr functions use recursion rather than iteration
// (C++14 constexpr functions may use local variables and loops)
constexpr int factorial(int n)
{
return n <= 1 ? 1 : (n * factorial(n - 1));
}
// literal class
class conststr {
const char* p;
std::size_t sz;
public:
template<std::size_t N>
constexpr conststr(const char(&a)[N]): p(a), sz(N - 1) {}
// constexpr functions signal errors by throwing exceptions
// in C++11, they must do so from the conditional operator ?:
constexpr char operator[](std::size_t n) const
{
return n < sz ? p[n] : throw std::out_of_range("");
}
constexpr std::size_t size() const { return sz; }
};
// C++11 constexpr functions had to put everything in a single return statement
// (C++14 doesn't have that requirement)
constexpr std::size_t countlower(conststr s, std::size_t n = 0,
std::size_t c = 0)
{
return n == s.size() ? c :
'a' <= s[n] && s[n] <= 'z' ? countlower(s, n + 1, c + 1) :
countlower(s, n + 1, c);
}
// output function that requires a compile-time constant, for testing
template<int n>
struct constN{
constN() { std::cout << n << '\n'; }
};
int main()
{
std::cout << "4! = " ;
constN<factorial(4)> out1; // computed at compile time
volatile int k = 8; // disallow optimization using volatile
std::cout << k << "! = " << factorial(k) << '\n'; // computed at run time
std::cout << "the number of lowercase letters in \"Hello, world!\" is ";
constN<countlower("Hello, world!")> out2; // implicitly converted to conststr
}
6.11 整型
6.11.1【建议】使用 <stdint.h> 中长度精确的整型
【说明】
C++ 内建整型中, 仅使用 int
. 如果程序中需要不同大小的变量, 可以使用 <stdint.h>
中长度精确的整型, 如 int16_t
如果您的变量可能不小于 2^31 (2GiB), 就用 64 位变量比如 int64_t
。
此外要留意,哪怕你的值并不会超出 int
所能够表示的范围,在计算过程中也可能会溢出。所以拿不准时,干脆用更大的类型。
无符号整数适用于表示位域和模运算。由于历史原因,C++标准还使用无符号整数来表示容器的大小——标准体的许多成员认为这是一个错误,但实际上不可能在这一点上进行修正。无符号算术不模拟简单整数的行为,而是由标准的模块化算术建模(环绕上溢/下溢)定义,这意味着编译器无法诊断一类重要的错误。在其他情况下,定义的行为会阻碍优化。
也就是说,混合整数类型的符号性导致了同样大的一类问题。我们能提供的最好建议是:尽量使用迭代器和容器,而不是指针和大小,尽量不要混合有符号,尽量避免无符号类型(表示位字段或模算术除外)。不要仅仅使用无符号类型来断言变量是非负的。
【代码示例】
#include <iostream>
int main()
{
int i = -1;
unsigned int j = 1;
if ( i < j ) // ops! compare between signed and unsigned
std::cout << " i is less than j";
else
std::cout << " i is greater than j";
return 0;
}
// Output: i is greater than j
6.12 64 位下的可移植性
6.12.1【建议】代码应该对 64 位和 32 位系统友好. 处理打印, 比较, 结构体对齐时应切记
【说明】
- Correct portable printf() conversion specifiers for some integral typedefs rely on macro expansions that we find unpleasant to use and impractical to require (the PRI macros from
). Unless there is no reasonable alternative for your particular case, try to avoid or even upgrade APIs that rely on the printf family. Instead use a library supporting typesafe numeric formatting, such as StrCat or Substitute for fast simple conversions, or std::ostream.
Unfortunately, the PRI macros are the only portable way to specify a conversion for the standard bitwidth typedefs (e.g., int64_t, uint64_t, int32_t, uint32_t, etc). Where possible, avoid passing arguments of types specified by bitwidth typedefs to printf-based APIs. Note that it is acceptable to use typedefs for which printf has dedicated length modifiers, such as size_t (z), ptrdiff_t (t), and maxint_t (j). - Remember that sizeof(void *) != sizeof(int). Use intptr_t if you want a pointer-sized integer.
- You may need to be careful with structure alignments, particularly for structures being stored on disk. Any class/structure with a int64_t/uint64_t member will by default end up being 8-byte aligned on a 64-bit system. If you have such structures being shared on disk between 32-bit and 64-bit code, you will need to ensure that they are packed the same on both architectures. Most compilers offer a way to alter structure alignment. For gcc, you can use attribute((packed)). MSVC offers #pragma pack() and __declspec(align()).
6.13 预处理宏
6.13.1【建议】使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之
【说明】
以下使用模式将避免宏的许多问题;如果使用宏,请尽可能遵循它:
- 不要在.h文件中定义宏。
- #在使用宏之前定义宏,然后立即取消定义宏。
int limit(int height)
{
#define MAX_HEIGHT 720
return std::max(height, MAX_HEIGHT);
#undef MAX_HEIGHT
}
- 在用自己的宏替换现有宏之前,不要只取消定义它;相反,选择一个可能是唯一的名称。Try not to use macros that expand to unbalanced C++ constructs, or at least document that behavior well.
- 好不要使用##来生成函数/类/变量名。
# macro can be replaced with constexpr
#define PI 3.14
# constexpr version
constexpr auto PI = 3.14;
# macro version
#define MAX(a,b) (((a)>(b))?(a):(b))
# constexpr version
template <typename T1, typename T2>
constexpr auto MAX(T1 a, T2 b) { return a > b ? a : b; }
极不鼓励从头文件导出宏(即在头文件中定义宏,而不在头文件末尾之前取消定义宏)。如果从头文件导出宏,它必须具有全局唯一的名称。要实现这一点,必须使用包含项目名称空间名称(但大写)的前缀来命名它。
6.14 0
, nullptr
和 NULL
6.14.1【规范】整数用 0
, 实数用 0.0
, 指针用 nullptr
或 NULL
, 字符 (串) 用 '\0'
6.14.2 【建议】对于指针,一律使用nullptr
6.14.3【规范】对于float,初始化时要加上f,比如: float f = 0.1f;
【说明】
对于指针 (地址值),到底是用 0
, NULL
还是 nullptr
。 C11 项目用 nullptr
; C03 项目则用 NULL
, 毕竟它看起来像指针。实际上,一些 C++ 编译器对 NULL 的定义比较特殊,可以输出有用的警告,特别是 sizeof(NULL)
就和 sizeof(0)
不一样。
字符 (串) 用 '\0', 不仅类型正确而且可读性好。
6.15 sizeof
6.15.1【建议】尽可能用 sizeof(varname) 代替 sizeof(type)
【说明】
使用 sizeof(varname) 是因为当代码中变量类型改变时会自动更新. 您或许会用 sizeof(type) 处理不涉及任何变量的代码,比如处理来自外部或内部的数据格式,这时用变量就不合适了。
6.16 类型推导
6.16.1【建议】用 auto 绕过烦琐的类型名,只要可读性好就继续用,别用在局部变量之外的地方
【说明】
只有当不熟悉项目的读者能够更清楚地理解代码,或者使代码更安全时,才使用类型推导。不要仅仅为了避免编写显式类型带来的不便而使用它。
基本规则是:使用类型推导只是为了使代码更清晰或更安全,而不是仅仅为了避免编写显式类型带来的不便。在判断代码是否更清晰时,请记住,您的读者不一定在您的团队中,也不一定熟悉您的项目,因此您和您的审阅者认为不必要的混乱类型通常会为其他人提供有用的信息。例如,您可以假设make_unique
6.17 class template argument deduction
c++11不支持,略
6.18 Designated Initializers
c++11不支持,略
struct Point {
float x = 0.0;
float y = 0.0;
float z = 0.0;
};
Point p = {
.x = 1.0,
.y = 2.0,
// z will be 0.0
};
6.19 Lambda 表达式
6.19.1【规范】适当使用 lambda 表达式。别用默认 lambda 捕获,所有捕获都要显式写出来。注意捕获对象的声明周期
【说明】
- 仅当lambda的生存期明显短于任何潜在捕获时,才使用默认的引用捕获([&])。
- 使用默认值捕获([=])仅作为为短lambda绑定几个变量的一种方法,其中捕获的变量集必须一目了然。不应该使用默认值捕获编写长的或复杂的lambda。
- 仅使用捕获来实际捕获封闭范围中的变量。不要使用带有初始值设定项的捕获来引入新名称,或实质性地更改现有名称的含义。相反,用传统的方法声明一个新变量,然后捕获它,或者直接显式地定义一个函数对象。
【代码示例】
相较于:
{
Foo foo;
...
executor->Schedule([&] { Frobnicate(foo); })
...
}
// BAD! The fact that the lambda makes use of a reference to `foo` and
// possibly `this` (if `Frobnicate` is a member function) may not be
// apparent on a cursory inspection. If the lambda is invoked after
// the function returns, that would be bad, because both `foo`
// and the enclosing object could have been destroyed.
更倾向于:
{
Foo foo;
...
executor->Schedule([&foo] { Frobnicate(foo); })
...
}
// BETTER - The compile will fail if `Frobnicate` is a member
// function, and it's clearer that `foo` is dangerously captured by
// reference.
6.20 模板编程
6.20.1【建议】不要使用复杂的模板编程
6.21 不应忽略警告信息
6.21.1【规范】 不应忽视编译器给出的warning信息,使用-Werror编译选项强制要求修复warning
7 命名约定
名称的样式立即通知我们命名实体是什么类型的:类型、变量、函数、常量、宏等,而不需要我们搜索该实体的声明。我们大脑中的模式匹配引擎在很大程度上依赖于这些命名规则。
7.1 通用命名规则
使用甚至对不同团队的人员都清晰的名称来优化可读性。
使用描述对象用途或意图的名称。别心疼空间,因为让新的读者立即理解您的代码更为重要。尽量减少使用项目外部人员可能不知道的缩写词(尤其是首字母缩略词和首字母缩略词)。
首选将缩写当做为单个单词,例如 StartRpc()
而不是 StartRPC()
。
7.1.1【规范】repo 风格要保证相对统一
【说明】
- 文件命名
【建议】全部小写,单词间使用"_"连接,如:file_name.h
,头文件应该以.h,.hpp或者.inc结尾,文件应该以.cc,.cpp或.h为后缀。不要和标准库的文件重名。
【说明】一种非常常见的情况是有一对名为 foo_bar.h
和 foo_bar.cc
的文件,它们定义了一个名为 FooBar
的类。
- 类型命名
【建议】单词首字母大写,如 MyClass。适用于:classes, structs, type aliases, enums, and type template parameters。多考虑使用using 替换 typedef。
【示例】
class MyClass {}; // class|
struct MyStruct {}; // struct
typedef int64_t TimeStamp; // type alias
using TimeStamp = int64_t; // type alias
template <typename Item> // type template parameter
class Container {};
- 变量
【建议】全部小写,单词间以"_"连接,如:param_var
- 类成员变量
【建议】全部小写,单词间以""连接,以""结尾,如: mem_var_
- 结构体成员变量
【建议】全部小写,单词间以""连接,如: mem_var
静态变量
【建议】在成员变量的命名规则基础上,添加前缀s,如:s_mem_var_
- 函数
【建议】单词首字母大写,对于缩写,值对首字母大写,如:DoFpn
而非 DoFPN
-
常量和枚举
【建议】枚举名命名规则同类型命名。以“k”开头,单词首字母大写,如:kEnumName,与宏的命名区分开(新版修改,旧版本采用和宏相同的命名规则,导致了两者命名冲突。) -
宏
【规范】全部大写,单词间使用"_"连接,如MACRO_NAME。 -
命名空间
【规范】全部小写,单词间以"_"连接。顶级命名空间需要基于项目名。
避免使用与已知顶级名称空间相同的嵌套名称空间。由于名称查找规则,命名空间名称之间的冲突可能导致意外的生成中断。特别是,不要创建任何嵌套的std命名空间。首选唯一的项目标识符(websearch::index,websearch::index_util)而不是像websearch::util这样容易发生冲突的名称。还要避免嵌套过深的命名空间.
对于internal名称空间,使用文件名生成唯一的内部名称(websearch::index::frobber_internal用于frobber.h)。
8 注释
8.1 注释风格
8.1.1【建议】对于作为接口暴露的头文件,需要尽可能使用doxygen工具的注释语法
8.1.2【建议】使用 //
,注释总是统一在代码上面
【说明】
不要使用"/* */"
进行块注释,这样在代码 review 时,无法准确看出哪些代码被注释了。
8.2 文件注释
8.2.1【规范】在每一个文件开头加入版权公告
【说明】
【代码示例】
/*
* Copyright (C) Horizon Robotics, Inc. - All Rights Reserved
* Unauthorized copying of this file, via any medium is strictly prohibited
* Proprietary and confidential
*/
8.2.2 文件注释描述了该文件的内容. 如果一个文件只声明, 或实现, 或测试了一个对象, 并且这个对象已经在它的声明处进行了详细的注释, 那么就没必要再加上文件注释. 除此之外的其他文件都需要文件注释.
8.3 类注释
8.3.1【规范】每个类的定义都要附带一份注释, 描述类的功能和用法, 除非它的功能相当明显
【说明】
- 类注释应该为读者提供足够的信息,来了解如何和何时使用该类,以及正确使用该类所需的任何其他注意事项。同时包含使用此类的前提(如果有)。
- 如果类的一个实例可以被多个线程访问,请特别注意记录多线程使用的规则和不变量。
- 类注释通常是一个用于展示类的简单用法的小示例代码片段的好地方。
【代码示例】
// Iterates over the contents of a GargantuanTable.
// Example:
// std::unique_ptr<GargantuanTableIterator> iter = table->NewIterator();
// for (iter->Seek("foo"); !iter->done(); iter->Next()) {
// process(iter->key(), iter->value());
// }
class GargantuanTableIterator {
...
};
8.4 函数注释
8.4.1【建议】函数声明处的注释描述函数功能
【说明】
- 几乎每个函数声明前面都应该有注释,用于描述函数的功能和使用方法。只有当函数简单且明显时(例如,类的明显属性的简单访问器),才可以省略这些注释。
- 功能注释应以该功能的隐含主语书写,并应以动词短语开头;例如,“Opens the file”,而不是“Open the file”。通常,功能注释不描述函数如何执行其任务。相反,这应该留给函数定义中的注释。
- 在编写函数重写(override)相关注释时,请关注重写本身的细节,而不是重复被重写函数的注释。在许多情况下,重写不需要额外的文档,因此不需要注释。
- 在注释构造函数和析构函数时,请记住,阅读代码的人知道构造函数和析构函数的用途,因此只说“销毁此对象”之类的注释是没有用的。记录构造函数对其参数所做的操作(例如,如果它们拥有指针的所有权),以及析构函数所做的清理。如果这是微不足道的,只需跳过评论。析构函数没有标题注释是很常见的。
- 在函数声明的注释中应该提到的内容:
- 输入和输出是什么。
- 对于类成员函数:对象在方法调用之后是否还有持有其参数的引用,以及是否会释放它们。
- 函数分配调用者是否必须释放的内存。
- 是否有任何参数可以是空指针。
- 函数的使用方式是否对性能有任何影响。
- 函数是否是可重入函数。它的同步前提是什么?
【代码示例】
// Returns an iterator for this table, positioned at the first entry
// lexically greater than or equal to `start_word`. If there is no
// such entry, returns a null pointer. The client must not use the
// iterator after the underlying GargantuanTable has been destroyed.
//
// This method is equivalent to:
// std::unique_ptr<Iterator> iter = table->NewIterator();
// iter->Seek(start_word);
// return iter;
std::unique_ptr<Iterator> GetIterator(absl::string_view start_word) const;
8.4.2【建议】定义处的注释描述函数实现
【说明】
- 如果函数如何工作有什么技巧性的地方,函数定义应该有解释性的注释。例如,在定义注释中,您可以描述您使用的任何编码技巧,概述您所经历的步骤,或者解释为什么您选择以您所做的方式而不是使用可行的替代方案来实现该功能。例如,您可能会提到为什么它必须为函数的前半部分获取锁,但为什么后半部分不需要锁。
- 注意:您不应该只在.h文件或任何地方重复函数声明中给出的注释。简单地概括一下函数的作用是可以的,但是注释的重点应该是它是如何实现的。
8.5 变量注释
8.5.1【建议】通常变量名本身足以很好说明变量用途. 某些情况下, 也需要额外的注释说明。注释格式遵循Doxygen。使用行注释
【代码示例】
/// Detailed description bdefore the member
int var;
8.6 实现注释
8.6.1【建议】对于代码中巧妙的, 晦涩的, 有趣的, 重要的地方加以注释
【代码示例】
// Divide result by two, taking into account that x
// contains the carry from the add.
for (int i = 0; i < result->size(); ++i) {
x = (x << 8) + (*result)[i];
(*result)[i] = x >> 1;
x &= 1;
}
8.7 标点 拼写和语法
8.7.1【建议】注意标点, 拼写和语法; 写的好的注释比差的要易读的多.
【说明】
注释的通常写法是包含正确大小写和结尾句号的完整叙述性语句。大多数情况下,完整的句子比句子片段可读性更高。短一点的注释,比如代码行尾注释,可以随意点,但依然要注意风格的一致性。
虽然被别人指出该用分号时却用了逗号多少有些尴尬,但清晰易读的代码还是很重要的。正确的标点,拼写和语法对此会有很大帮助。
8.8 TODO 注释
对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释。
8.8.1【规范】满足一定的格式(cpplint):TODO(zhan.shi)
【说明】
格式:大写TODO,使用圆括号,然后冒号,再空格,圆括号里的内容是邮箱前缀(不是姓名前缀)。冒号后需要写明后续action。
【代码示例】
// TODO(zhan.shi): Use a "*" here for concatenation operator.
8.8.2【规范】最终 release 版本时,不能有TODO 相关的代码注释
8.9 弃用注释
8.9.1【规范】通过弃用注释(DEPRECATED comments)以标记某接口点已弃用.
【说明】
弃用注释应当包涵简短而清晰的指引,以帮助其他人修复其调用点。在 C++ 中,你可以将一个弃用函数改造成一个内联函数,这一函数将调用新的接口。
格式:大写DEPRECATED
,使用圆括号,然后冒号,再空格,圆括号里的内容是邮箱前缀(不是姓名前缀)。后接具体说明。
【代码示例】
//DEPRECATED(dablelv):new interface changed to bool IsTableFull(const Table& t)
bool IsTableFull();
9 格式
9.1 行长度
9.1.1【规范】一行最多120个字符
9.2 非ASCII字符
9.1.1【规范】代码中不要添加非ASCII的字符,允许中文注释
9.3 空格还是制表位
9.3.1【规范】缺省缩进为 2 个空格.使用空格而不是tab
9.4 函数声明和定义
9.4.1【规范】对显式重写的虚函数要使用override修饰。重写虚函数时不要添加virtual关键字
【代码示例】
class Base {
public:
virtual void do_something() {}
};
class Derived :public Base {
public:
void do_something() override {} // good
virtual void do_something() override {} // bad: remove virtual
};
9.4.2【规范】所有形参应尽可能对齐,如果第一个参数无法和函数名放在同一行,则换行后的参数保持 4 个空格的缩进
【代码示例】
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
Type par_name3) {
DoSomething();
...
}
ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
Type par_name1, // 4 space indent
Type par_name2,
Type par_name3) {
DoSomething(); // 2 space indent
...
}
9.4.3【建议】只有在参数未被使用或者其用途非常明显时, 才能省略参数名
【代码示例】
class Foo {
public:
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
};
9.4.4【建议】如果返回类型和函数名在一行放不下,分行
【代码示例】
分行的格式参考如下
int
mappages(pagetable_t pagetable, uint64 va,
uint64 size, uint64 pa, int perm) {
}
9.4.5【建议】如果返回类型与函数声明或定义分行了, 不要缩进
【代码示例】
int
mappages() {
...
}
9.4.6【建议】左圆括号总是和函数名在同一行
【代码示例】
int AVeryVeryVeryVeryLongFunctionName(
pagetable_t pagetable, uint64 va, // 4 spaces
uint64 size, uint64 pa, int perm) {
// do something
}
而不是
int AVeryVeryVeryVeryLongFunctionName
(pagetable_t pagetable, uint64 va,
uint64 size, uint64 pa, int perm) {
// do something
// do something
}
9.4.7【建议】函数名和左圆括号间永远没有空格
【代码示例】
int FunctionName() {
// do something
}
而不是
int FunctionName () {
// do something
}
9.4.8【建议】圆括号与参数间没有空格
【代码示例】
int FunctionName(pagetable_t pagetable) {
// do something
}
而不是
int FunctionName( pagetable_t pagetable ) {
// do something
}
9.4.9【建议】左大括号总在最后一个参数同一行的末尾处,不另起新行
【代码示例】
int FunctionName(
pagetable_t pagetable, uint64 va,
uint64 size, uint64 pa, int perm) {
// do something
}
而不是
int FunctionName(
pagetable_t pagetable, uint64 va,
uint64 size, uint64 pa, int perm)
{
// do something
}
9.4.10【建议】右大括号总是单独位于函数最后一行, 或者与左大括号同一行
【代码示例】
int FunctionName(
pagetable_t pagetable, uint64 va,
uint64 size, uint64 pa, int perm) {
do something();
}
void SmallFunction() { do_something(); }
而不是
int FunctionName(
pagetable_t pagetable, uint64 va,
uint64 size, uint64 pa, int perm) {
do something();}
void SmallFunction() { do_something();
}
9.4.11【建议】右圆括号和左大括号间总是有一个空格
【代码示例】
int FunctionName(
pagetable_t pagetable, uint64 va,
uint64 size, uint64 pa, int perm) {
do something();
}
而不是
int FunctionName(
pagetable_t pagetable, uint64 va,
uint64 size, uint64 pa, int perm){
do something();
}
9.5 Lambda表达式
9.5.1【建议】参数的格式和普通函数相同
【代码示例】
auto pow_n = [](int n) -> int { return n * n; }
9.5.2【建议】引用捕获时,&和变量名之间不应有空格
【代码示例】
int x = 0;
auto x_plus_n = [&x](int n) -> int { return x + n; }
9.5.3【建议】短lambda可以内联编写为函数参数,这时候也需要遵守函数参数的格式要求
【代码示例】
std::set<int> to_remove = {7, 8, 9};
std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&to_remove](int i) {
return to_remove.find(i) != to_remove.end();
}),
digits.end());
9.6 浮点字面量
9.6.1【建议】浮点字面量应该始终有一个基数,两边都有数字,即使它们使用指数表示法
【代码示例】
float f = 1.0f;
float f2 = 1; // Also OK
long double ld = -0.5L;
double d = 1248.0e6;
而不是
float f = 1.f;
long double ld = -.5L;
double d = 1248e6;
9.7 条件
9.7.1【建议】倾向于不在圆括号内使用空格。关键字 if 和 else 另起一行
if (condition) {
DoOneThing();
} else {
DoNothing();
}
而不是
if (condition) DoOneThing(); else DoNothing();
9.7.2【规范】 有 else if 时,最后一分分支必须 是 else
【代码示例】
if (condition) { // no spaces inside parentheses, space before brace
DoOneThing(); // two space indent
DoAnotherThing();
} else if (int a = f(); a != 3) { // closing brace on new line, else on same line
DoAThirdThing(a);
} else {
DoNothing();
}
9.7.3【建议】单行条件语句或者循环也需要使用大括号
【代码示例】
不允许出现如下格式:
if (x) DoThis();
else DoThat();
而应该是:
if (x) {
DoThis();
} else {
DoThat();
}
9.7.4【建议】switch 语句中如果有不满足 case 条件的枚举值, switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理, 编译器将给出 warning). 如果 default 应该永远执行不到, 简单的加条 assert:
【代码示例】
switch (var) {
case 0: { // 2 space indent
... // 4 space indent
break;
}
case 1: {
...
break;
}
default: {
assert(false);
}
}
9.8 循环
9.8.1【建议】空循环体应使用 {} 或 continue, 而不是一个简单的分号
【代码示例】
while (condition) {
// do something
} // good
while (condition) continue; // good
while (condition) {} // good
while (condition) ; //bad
9.9 指针和引用表达式
9.9.1【建议】指针或引用表达式中,句点或箭头前后不要有空格. 指针/地址操作符 (*, &) 之后不能有空格。星号和引用符号与变量名紧挨;
【代码示例】
x = *p;
p = &x;
x = r.y;
x = r->y;
// These are fine, space preceding.
char *c;
const std::string &str;
int *GetPointer();
std::vector<char *>
而不是
// These are fine, space following (or elided).
char* c;
const std::string& str;
int* GetPointer();
std::vector<char*> // Note no space between '*' and '>'
9.8.2【规范】多重声明中不允许声明指针和引用
【代码示例】
不允许如下写法:
int *x, y; // 不允许
9.10 布尔表达式
9.10.1【规范】布尔表达式断行时,&&操作符必须位于行尾
【代码示例】
if (this_one_thing > this_other_thing &&
a_third_thing == a_fourth_thing &&
yet_another && last_one) {
...
}
而不是
if (this_one_thing > this_other_thing
&& a_third_thing == a_fourth_thing
&& yet_another && last_one) {
...
}
9.10.2【建议】有时参数形成的结构对可读性很重要。在这些情况下,可以根据该结构自由设置参数格式
【代码示例】
// Transform the widget by a 3x3 matrix.
my_widget.Transform(x1, x2, x3,
y1, y2, y3,
z1, z2, z3);
不要写这样的格式:
if (SomethingIsGood()) {
return true;
} else {
return false;
}
修改成:
return SomethinIsGood();
9.11 预处理指令
9.11.1【规范】预处理指令不要缩进, 从行首开始
【代码示例】
// Good - directives at beginning of line
if (lopsided_score) {
#if DISASTER_PENDING // Correct -- Starts at beginning of line
DropEverything();
# if NOTIFY // OK but not required -- Spaces after #
NotifyClient();
# endif
#endif
BackToNormal();
}
9.12 类格式
9.12.1【建议】访问控制块的声明依次序是 public:, protected:, private:, 每个都缩进 1 个空格。public 放在最前面, 然后是 protected, 最后是 private
【代码示例】
class StringHolder {
public:
// The input `val` must live as long as this object, not just the call to
// this c'tor
StringHolder(const string& val) : val_(val) {}
const string& Get() { return val_; }
protected:
void DoSomething();
private:
const string& val_;
};
9.13 命名空间
9.13.1【规范】命名空间内容不缩进
【代码示例】
namespace {
void foo() { // Correct. No extra indentation within namespace.
...
}
} // namespace a
namespace b {
namespace c {
} // namespace c
} // namepsace b
而不要这样
namespace a {
// Wrong! Indented when it should not be.
void foo() {
...
}
} // namespace a
namespace b {
namespace c {
} // namespace c
} // namepsace b
9.12.2【规范】命名空间结束需要添加注释注明所属的命名空间
【代码示例】
namespace test {
} // namespace test
9.12.3【建议】不要加入无用的空行。可以用空行划分代码逻辑段
9.14 变量和数组初始化
9.14.1【建议】使用 int x{3} 而不是 int x{ 3 }
【说明】
使用=, ()和{}进行初始化
int x = 3;
int x(3);
int x{3};
std::string name = "Some Name";
std::string name("Some Name");
std::string name{"Some Name"};
对于使用初始化列表的初始化需要额外小心,下面两种方式的行为是不一样的。
std::vector<int> v(100, 1); // A vector containing 100 items: All 1s.
std::vector<int> v{100, 1}; // A vector containing 2 items: 100 and 1.
使用{}进行初始化将不支持隐式转换:
int pi(3.14); // OK -- pi == 3.
int pi{3.14}; // Compile error: narrowing conversion.
9.14.2【建议】构造函数初始化列表,下面几个方式都可以
// When everything fits on one line:
MyClass::MyClass(int var) : some_var_(var) {
DoSomething();
}
// If the signature and initializer list are not all on one line,
// you must wrap before the colon and indent 4 spaces:
MyClass::MyClass(int var)
: some_var_(var), some_other_var_(var + 1) {
DoSomething();
}
// When the list spans multiple lines, put each member on its own line
// and align them:
MyClass::MyClass(int var)
: some_var_(var), // 4 space indent
some_other_var_(var + 1) { // lined up
DoSomething();
}
// As with any other code block, the close curly can be on the same
// line as the open curly, if it fits.
MyClass::MyClass(int var)
: some_var_(var) {}
9.15 操作符
9.15.1【建议】建议每个二元操作符间添加空格
【代码示例】
// Assignment operators always have spaces around them.
x = 0;
// Other binary operators usually have spaces around them, but it's
// OK to remove spaces around factors. Parentheses should have no
// internal padding.
v = w * x + y / z; // good
v = w*x + y/z; // bad
v = w * (x + z); // good
// No spaces separating unary operators and their arguments.
x = -5;
++x;
if (x && !y)
...
9.16 整体示例
// Copyright 2014 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef RCLCPP__NODE_IMPL_HPP_
#define RCLCPP__NODE_IMPL_HPP_
#include <rmw/error_handling.h>
#include <rmw/rmw.h>
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <limits>
#include <map>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "rcl/publisher.h"
#include "rcl/subscription.h"
#include "rclcpp/contexts/default_context.hpp"
#include "rclcpp/create_client.hpp"
#include "rclcpp/create_generic_publisher.hpp"
#include "rclcpp/create_generic_subscription.hpp"
#include "rclcpp/create_publisher.hpp"
#include "rclcpp/create_service.hpp"
#include "rclcpp/create_subscription.hpp"
#include "rclcpp/create_timer.hpp"
#include "rclcpp/detail/resolve_enable_topic_statistics.hpp"
#include "rclcpp/parameter.hpp"
#include "rclcpp/qos.hpp"
#include "rclcpp/timer.hpp"
#include "rclcpp/type_support_decl.hpp"
#include "rclcpp/visibility_control.hpp"
#ifndef RCLCPP__NODE_HPP_
#include "node.hpp"
#endif
namespace rclcpp
{
/// Node is the single point of entry for creating publishers and subscribers.
class Node : public std::enable_shared_from_this<Node>
{
public:
using OnSetParametersCallbackHandle =
rclcpp::node_interfaces::OnSetParametersCallbackHandle;
using OnParametersSetCallbackType =
rclcpp::node_interfaces::NodeParametersInterface::OnParametersSetCallbackType;
RCLCPP_SMART_PTR_DEFINITIONS(Node)
/// Create a new node with the specified name.
/**
* \param[in] node_name Name of the node.
* \param[in] options Additional options to control creation of the node.
* \throws InvalidNamespaceError if the namespace is invalid
*/
RCLCPP_PUBLIC
explicit Node(
const std::string &node_name,
const NodeOptions &options = NodeOptions());
/// Create a new node with the specified name.
/**
* \param[in] node_name Name of the node.
* \param[in] namespace_ Namespace of the node.
* \param[in] options Additional options to control creation of the node.
* \throws InvalidNamespaceError if the namespace is invalid
*/
RCLCPP_PUBLIC
explicit Node(
const std::string &node_name,
const std::string &namespace_,
const NodeOptions &options = NodeOptions());
RCLCPP_PUBLIC
virtual ~Node();
/// Get the name of the node.
/** \return The name of the node. */
RCLCPP_PUBLIC
const char *
GetName() const;
/// Get the namespace of the node.
/**
* This namespace is the "node's" namespace, and therefore is not affected
* by any sub-namespace's that may affect entities created with this instance.
* Use get_effective_namespace() to get the full namespace used by entities.
*
* \sa get_sub_namespace()
* \sa get_effective_namespace()
* \return The namespace of the node.
*/
RCLCPP_PUBLIC
const char *
GetNamespace() const;
/// Get the fully-qualified name of the node.
/**
* The fully-qualified name includes the local namespace and name of the node.
* \return fully-qualified name of the node.
*/
RCLCPP_PUBLIC
const char *
GetFullyQualifiedName() const;
/// Get the logger of the node.
/** \return The logger of the node. */
RCLCPP_PUBLIC
rclcpp::Logger
GetLogger() const;
/// Create and return a callback group.
RCLCPP_PUBLIC
rclcpp::CallbackGroup::SharedPtr
CreateCallbackGroup(
rclcpp::CallbackGroupType group_type,
bool automatically_add_to_executor_with_node = true);
/// Return the list of callback groups in the node.
RCLCPP_PUBLIC
const std::vector<rclcpp::CallbackGroup::WeakPtr> &
GetCallbackGroups() const;
/// Create and return a Publisher.
/**
* The rclcpp::QoS has several convenient constructors, including a
* conversion constructor for size_t, which mimics older API's that
* allows just a string and size_t to create a publisher.
*
* For example, all of these cases will work:
*
* ```cpp
* pub = node->create_publisher<MsgT>("chatter", 10); // implicitly KeepLast
* pub = node->create_publisher<MsgT>("chatter", QoS(10)); // implicitly KeepLast
* pub = node->create_publisher<MsgT>("chatter", QoS(KeepLast(10)));
* pub = node->create_publisher<MsgT>("chatter", QoS(KeepAll()));
* pub = node->create_publisher<MsgT>("chatter", QoS(1).best_effort().durability_volatile());
* {
* rclcpp::QoS custom_qos(KeepLast(10), rmw_qos_profile_sensor_data);
* pub = node->create_publisher<MsgT>("chatter", custom_qos);
* }
* ```
*
* The publisher options may optionally be passed as the third argument for
* any of the above cases.
*
* \param[in] topic_name The topic for this publisher to publish on.
* \param[in] qos The Quality of Service settings for the publisher.
* \param[in] options Additional options for the created Publisher.
* \return Shared pointer to the created publisher.
*/
template<
typename MessageT,
typename AllocatorT = std::allocator<void>,
typename PublisherT = rclcpp::Publisher<MessageT, AllocatorT>>
std::shared_ptr<PublisherT>
CreatePublisher(
const std::string &topic_name,
const rclcpp::QoS &qos,
const PublisherOptionsWithAllocator<AllocatorT> &options =
PublisherOptionsWithAllocator<AllocatorT>()
);
/// Create and return a Subscription.
/**
* \param[in] topic_name The topic to subscribe on.
* \param[in] qos QoS profile for Subcription.
* \param[in] callback The user-defined callback function to receive a message
* \param[in] options Additional options for the creation of the Subscription.
* \param[in] msg_mem_strat The message memory strategy to use for allocating messages.
* \return Shared pointer to the created subscription.
*/
template<
typename MessageT,
typename CallbackT,
typename AllocatorT = std::allocator<void>,
typename SubscriptionT = rclcpp::Subscription<MessageT, AllocatorT>,
typename MessageMemoryStrategyT = typename SubscriptionT::MessageMemoryStrategyType
>
std::shared_ptr<SubscriptionT>
CreateSubscription(
const std::string &topic_name,
const rclcpp::QoS &qos,
CallbackT &&callback,
const SubscriptionOptionsWithAllocator<AllocatorT> &options =
SubscriptionOptionsWithAllocator<AllocatorT>(),
typename MessageMemoryStrategyT::SharedPtr msg_mem_strat = (
MessageMemoryStrategyT::create_default()
)
);
/// Create a timer.
/**
* \param[in] period Time interval between triggers of the callback.
* \param[in] callback User-defined callback function.
* \param[in] group Callback group to execute this timer's callback in.
*/
template<typename DurationRepT = int64_t, typename DurationT = std::milli, typename CallbackT>
typename rclcpp::WallTimer<CallbackT>::SharedPtr
CreateWallTimer(
std::chrono::duration<DurationRepT, DurationT> period,
CallbackT callback,
rclcpp::CallbackGroup::SharedPtr group = nullptr);
/// Declare and initialize a parameter with a type.
/**
* See the non-templated DeclareParameter() on this class for details.
*
* If the type of the default value, and therefore also the type of return
* value, differs from the initial value provided in the node options, then
* a rclcpp::exceptions::InvalidParameterTypeException may be thrown.
* To avoid this, use the DeclareParameter() method which returns an
* rclcpp::ParameterValue instead.
*
* Note, this method cannot return a const reference, because extending the
* lifetime of a temporary only works recursively with member initializers,
* and cannot be extended to members of a class returned.
* The return value of this class is a copy of the member of a ParameterValue
* which is returned by the other version of DeclareParameter().
* See also:
*
* - https://en.cppreference.com/w/cpp/language/lifetime
* - https://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/
* - https://www.youtube.com/watch?v=uQyT-5iWUow (cppnow 2018 presentation)
*/
template<typename ParameterT>
auto
DeclareParameter(
const std::string & name,
const ParameterT & default_value,
const rcl_interfaces::msg::ParameterDescriptor & parameter_descriptor =
rcl_interfaces::msg::ParameterDescriptor(),
bool ignore_override = false);
/// Get the parameter values for all parameters that have a given prefix.
/**
* The "prefix" argument is used to list the parameters which are prefixed
* with that prefix, see also list_parameters().
*
* The resulting list of parameter names are used to get the values of the
* parameters.
*
* The names which are used as keys in the values map have the prefix removed.
* For example, if you use the prefix "foo" and the parameters "foo.ping" and
* "foo.pong" exist, then the returned map will have the keys "ping" and
* "pong".
*
* An empty string for the prefix will match all parameters.
*
* If no parameters with the prefix are found, then the output parameter
* "values" will be unchanged and false will be returned.
* Otherwise, the parameter names and values will be stored in the map and
* true will be returned to indicate "values" was mutated.
*
* This method will never throw the
* rclcpp::exceptions::ParameterNotDeclaredException exception because the
* action of listing the parameters is done atomically with getting the
* values, and therefore they are only listed if already declared and cannot
* be undeclared before being retrieved.
*
* Like the templated get_parameter() variant, this method will attempt to
* coerce the parameter values into the type requested by the given
* template argument, which may fail and throw an exception.
*
* \param[in] prefix The prefix of the parameters to get.
* \param[out] values The map used to store the parameter names and values,
* respectively, with one entry per parameter matching prefix.
* \returns true if output "values" was changed, false otherwise.
* \throws rclcpp::ParameterTypeException if the requested type does not
* match the value of the parameter which is stored.
*/
template<typename ParameterT>
bool
GetParameters(
const std::string & prefix,
std::map<std::string, ParameterT> & values) const;
/// Add a callback for when parameters are being set.
/**
* The callback signature is designed to allow handling of any of the above
* `set_parameter*` or `DeclareParameter*` methods, and so it takes a const
* reference to a vector of parameters to be set, and returns an instance of
* rcl_interfaces::msg::SetParametersResult to indicate whether or not the
* parameter should be set or not, and if not why.
*
* For an example callback:
*
* ```cpp
* rcl_interfaces::msg::SetParametersResult
* my_callback(const std::vector<rclcpp::Parameter> ¶meters)
* {
* rcl_interfaces::msg::SetParametersResult result;
* result.successful = true;
* for (const auto ¶meter : parameters) {
* if (!some_condition) {
* result.successful = false;
* result.reason = "the reason it could not be allowed";
* }
* }
* return result;
* }
* ```
*
* You can see that the SetParametersResult is a boolean flag for success
* and an optional reason that can be used in error reporting when it fails.
*
* This allows the node developer to control which parameters may be changed.
*
* It is considered bad practice to reject changes for "unknown" parameters as this prevents
* other parts of the node (that may be aware of these parameters) from handling them.
*
* Note that the callback is called when DeclareParameter() and its variants
* are called, and so you cannot assume the parameter has been set before
* this callback, so when checking a new value against the existing one, you
* must account for the case where the parameter is not yet set.
*
* Some constraints like read_only are enforced before the callback is called.
*
* The callback may introspect other already set parameters (by calling any
* of the {get,list,describe}_parameter() methods), but may *not* modify
* other parameters (by calling any of the {set,declare}_parameter() methods)
* or modify the registered callback itself (by calling the
* add_on_set_parameters_callback() method). If a callback tries to do any
* of the latter things,
* rclcpp::exceptions::ParameterModifiedInCallbackException will be thrown.
*
* The callback functions must remain valid as long as the
* returned smart pointer is valid.
* The returned smart pointer can be promoted to a shared version.
*
* Resetting or letting the smart pointer go out of scope unregisters the callback.
* `remove_on_set_parameters_callback` can also be used.
*
* The registered callbacks are called when a parameter is set.
* When a callback returns a not successful result, the remaining callbacks aren't called.
* The order of the callback is the reverse from the registration order.
*
* \param callback The callback to register.
* \returns A shared pointer. The callback is valid as long as the smart pointer is alive.
* \throws std::bad_alloc if the allocation of the OnSetParametersCallbackHandle fails.
*/
RCLCPP_PUBLIC
RCUTILS_WARN_UNUSED
OnSetParametersCallbackHandle::SharedPtr
DddOnSetParametersCallback(OnParametersSetCallbackType callback);
protected:
/// Construct a sub-node, which will extend the namespace of all entities created with it.
/**
* \sa create_sub_node()
*
* \param[in] other The node from which a new sub-node is created.
* \param[in] sub_namespace The sub-namespace of the sub-node.
*/
RCLCPP_PUBLIC
Node(
const Node &other,
const std::string &sub_namespace);
private:
RCLCPP_DISABLE_COPY(Node)
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_base_;
rclcpp::node_interfaces::NodeGraphInterface::SharedPtr node_graph_;
rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr node_logging_;
rclcpp::node_interfaces::NodeTimersInterface::SharedPtr node_timers_;
rclcpp::node_interfaces::NodeTopicsInterface::SharedPtr node_topics_;
rclcpp::node_interfaces::NodeServicesInterface::SharedPtr node_services_;
rclcpp::node_interfaces::NodeClockInterface::SharedPtr node_clock_;
rclcpp::node_interfaces::NodeParametersInterface::SharedPtr node_parameters_;
rclcpp::node_interfaces::NodeTimeSourceInterface::SharedPtr node_time_source_;
rclcpp::node_interfaces::NodeWaitablesInterface::SharedPtr node_waitables_;
const rclcpp::NodeOptions node_options_;
const std::string sub_namespace_;
const std::string effective_namespace_;
};
RCLCPP_LOCAL
inline
std::string
ExtendNameWithSubNamespace(const std::string &name, const std::string &sub_namespace)
{
std::string name_with_sub_namespace(name);
if (sub_namespace != "" &&name.front() != '/' &&name.front() != '~') {
name_with_sub_namespace = sub_namespace + "/" + name;
}
return name_with_sub_namespace;
}
template<typename MessageT, typename AllocatorT, typename PublisherT>
std::shared_ptr<PublisherT>
Node::CreatePublisher(
const std::string &topic_name,
const rclcpp::QoS &qos,
const PublisherOptionsWithAllocator<AllocatorT> &options)
{
return rclcpp::create_publisher<MessageT, AllocatorT, PublisherT>(
*this,
extend_name_with_sub_namespace(topic_name, this->get_sub_namespace()),
qos,
options);
}
template<
typename MessageT,
typename CallbackT,
typename AllocatorT,
typename SubscriptionT,
typename MessageMemoryStrategyT>
std::shared_ptr<SubscriptionT>
Node::CreateSubscription(
const std::string &topic_name,
const rclcpp::QoS &qos,
CallbackT &&callback,
const SubscriptionOptionsWithAllocator<AllocatorT> &options,
typename MessageMemoryStrategyT::SharedPtr msg_mem_strat)
{
return rclcpp::create_subscription<MessageT>(
*this,
extend_name_with_sub_namespace(topic_name, this->get_sub_namespace()),
qos,
std::forward<CallbackT>(callback),
options,
msg_mem_strat);
}
template<typename DurationRepT, typename DurationT, typename CallbackT>
typename rclcpp::WallTimer<CallbackT>::SharedPtr
Node::CreateWallTimer(
std::chrono::duration<DurationRepT, DurationT> period,
CallbackT callback,
rclcpp::CallbackGroup::SharedPtr group)
{
return rclcpp::create_wall_timer(
period,
std::move(callback),
group,
this->node_base_.get(),
this->node_timers_.get());
}
template<typename ParameterT>
auto
Node::DeclareParameter(
const std::string &name,
const rcl_interfaces::msg::ParameterDescriptor ¶meter_descriptor,
bool ignore_override)
{
// get advantage of parameter value template magic to get
// the correct rclcpp::ParameterType from ParameterT
rclcpp::ParameterValue value{ParameterT{}};
return this->DeclareParameter(
name,
value.get_type(),
parameter_descriptor,
ignore_override
).get<ParameterT>();
}
// this is a partially-specialized version of get_parameter above,
// where our concrete type for ParameterT is std::map, but the to-be-determined
// type is the value in the map.
template<typename ParameterT>
bool
Node::GetParameters(
const std::string &prefix,
std::map<std::string, ParameterT> &values) const
{
std::map<std::string, rclcpp::Parameter> params;
bool result = node_parameters_->get_parameters_by_prefix(prefix, params);
if (result) {
for (const auto ¶m : params) {
values[param.first] = static_cast<ParameterT>(param.second.get_value<ParameterT>());
}
}
return result;
}
} // namespace rclcpp
#endif // RCLCPP__NODE_IMPL_HPP_
Q.E.D.