ABACUS 开源项目 C++ 代码规范
作者:韩昊知,邮箱:haozhi.han@stu.pku.edu.cn
审核:陈默涵,邮箱:mohanchen@pku.edu.cn
最后更新时间:2023/09/04
ABACUS(Atomic-orbtial Based Ab-initio Computation at UStc,中文名原子算筹)是国产开源密度泛函理论软件,相关介绍 ABACUS 的新闻可在 ABACUS 新闻稿整理查看。此文档用于给 ABACUS 开发者提供代码编程规范方面的建议。
前言
让任何开发者都可以快速读懂别人的代码,这点对于 ABACUS 项目很重要。此外,使代码易于管理的方法之一是加强代码一致性,这需要从代码规范开始着手。
一、命名约定
1. 普通变量的命名统一是小写,并使用下划线命名法
以下是四种命名方法:下划线命名法、匈牙利命名法、驼峰式命名法、帕斯卡命名法
<strong>string table_name; </strong>// 下划线命名法(推荐) - 用下划线区分单词(推荐使用)
string sTableName; // 匈牙利命名法 - 前缀字母用变量类型缩写,单词首字母大写
string tableName; // 驼峰命名法 - 混合大小写(不推荐使用)
string TableName; // 帕斯卡命名法 - 每个单词首字母大写(不推荐使用)
下划线命名法:推荐使用
匈牙利命名法:目前 ABACUS 中有些变量(例如指针),首字母为数据类型(指针为 p),可以考虑用这种方式命名,但不推荐用大写字母
驼峰命名法/帕斯卡命名法:目前 ABACUS 大部分的代码不是用这种命名方式的,为了代码风格统一,所以不推荐使用
2. 函数命名
建议使用下划线命名法且所有字母全部小写,建议小于 18 个字符
void calculate_area() {
// function body
}
3. private 的类数据成员在最后以下划线结尾
private 的类数据成员和普通变量的命名方式一样,但要在最后以下划线结尾,以区分自己是 private 的类数据成员。
class TableInfo
{
...
private:
string table_name_; // 好 - 后加下划线
string tablename_; // 好
static Pool<TableInfo>* pool_; // 好
};
4. 只有全局变量用全大写命名
在 ABACUS 中,例如 NBANDS(能带数),NLOCAL(局域轨道数目)这种全大写的变量名是全局变量,其它的不建议用大写字母给变量命名。另外,ABACUS 将通过重构或者删除的方式逐步淘汰全局变量,因此不建议增加新的全局变量。
二、关于头文件
1. 只有在真正需要使用某个库/头文件时才 #include 它们
不需要的 #include 删除。
2. #include避免使用快捷目录 . (当前目录) 或 .. (上级目录)
项目内头文件应按照项目源代码目录的树结构排列,避免使用 UNIX 特殊的快捷目录 . (当前目录) 或 .. (上级目录)。例如
source/module_hsolver/diago_cg.h 文件是这样引用 module_base 的头文件:
#include "diagh.h"
#include "module_base/complexmatrix.h"
3. #include 顺序(clang-format 可以自动)
使用标准的头文件包含顺序,这样可以可增强可读性,避免隐藏依赖。
foo.cpp中包含头文件的次序如下:
- 与源文件对应的头文件:
dir2/foo2.h(这种优先的顺序排序保证当dir2/foo2.h遗漏某些必要的库时,dir/foo.cpp的构建会立刻中止)- C 系统文件
- C++ 系统文件
- 其他库的
.h文件- 本项目内
.h文件
4. 头文件需要 #define 保护
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_
5. 避免使用前置声明
使用 #include 包含需要的头文件,尽量避免使用前置声明(见以下例子解释什么是前置声明)。
// 什么是前置声明?
// MyClassB.h
// Bad: Overuse of forward declarations
class MyClassB;
class MyClassA {
public:
void DoSomething(MyClassB* obj_b);
};
void MyClassA::DoSomething(MyClassB* obj_b) {
// ...
}
在这段代码中,
MyClassB被声明为一个类,但没有给出其定义。这被称为前置声明(Forward Declaration),它告诉编译器MyClassB是一个存在的类,但不提供该类的详细信息。
6. 当函数只有 10 行甚至更少时,才将其定义为内联函数
只有当函数只有 10 行甚至更少时才将其定义为内联函数(关键字 inline)。
三、关于类(Class)
1. 先声明类的成员变量,再声类的成员函数
2. 将public 部分放在最前,之后是protected数据,最后是private
3. 不使用运算符重载
若要使用,建议提交 issue 讨论
4. 什么时候用 struct?
只有数据成员、没有成员函数时可以用 struct
5. 类型转换使用static_cast<>()
建议使用 C++ 的类型转换,如 static_cast<>()。
不要使用 int y = (int)x 或 int y = int(x) 等转换方式。
- 用
static_cast替代 C 风格的值转换, 或某个类指针需要明确的向上转换为父类指针时。- 用
const_cast去掉const限定符。- 用
reinterpret_cast指针类型和整型或其它指针之间进行不安全的相互转换。 仅在你对所做一切了然于心时使用。
6. 继承采用 public 继承
如果你想使用私有继承,你应该替换成把基类的实例作为成员对象的方式去定义类。
7. 不过度使用实现继承,可以考虑组合
例如,如下案例:
class Engine {
public:
void start() { /* 启动引擎 */ }
};
class Car {
public:
Car(const std::string& n, Engine& e) : name_(n), engine_(e) {}
void drive() { engine_.start(); std::cout << name_ << " is driving\n"; }
private:
std::string name_;
Engine& engine_;
};
int main() {
Engine engine;
Car car("mycar", engine);
car.drive();
return 0;
}
四、关于函数(Function)
1. 函数传参:不变参数用 const 和 &
函数的输入参数与输出参数:
在一个函数中,不变的量,我们可以看作是函数的输入参数;变化的量,我们可以看作是函数的输出参数。
在输入参数中可以选择 const T* (指向常量对象的指针,不能通过这个指针来修改其指向的对象的值。然而,你可以改变指针本身的值)。
也可以使用 const T& (不能通过这个引用来修改其引用的对象的值,在其生命周期内不能重新引用另一个对象)。
所以,建议使用
const T&,若要使用const T*,则应给出相应的理由,否则会使读者感到迷惑。
void Foo(const string &in, string *out);
// 输入参数:in (const + 引用&)
// 输出参数:out(指针变量)
2. 函数传参:会变的参数用指针
什么是引用参数:
- 在 C 中, 如果函数需要修改输入变量的值, 参数必须为指针, 如
int foo(int *pval)。 - 在 C++ 中, 函数还可以声明为引用参数:
int foo(int &val)。
引用参数在语法上是值变量却拥有指针的语义(变量可以被改变!)。
3. 每个函数不超过 50 行
建议编写简短,凝练的函数,有特殊情况的除外。
4. 函数返回值(return 的值)多使用值返回和引用返回,避免使用指针返回
五、关于作用域
1. 变量要初始化
无初始化的变量可能会引起结果不稳定(例如出现随机数),因此建议养成习惯,对所有变量的值要初始化,见下面的例子:
int i;
i = f(); // 坏——初始化和声明分离
int j = f(); // 好——初始化时声明
vector<int> v; // 坏——初始化和声明分离
v.push_back(1);
v.push_back(2);
vector<int> v = {1, 2}; // 好——初始化时声明
2. 将局部变量置于最小作用域
在
if,while和for语句中:
变量不是对象,则变量应当在内部声明与初始化,这样子这些变量的作用域就被限制在这些语句中了,举例而言:
while (const char* p = strchr(str, '/')) { str = p + 1; }如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数。这会导致效率降低。建议将构造函数调用次数减少以提高程序效率。
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); }
3. 仅在局部作用域使用using namespace
使用 using namespace 语句来引入命名空间中的所有名称可能会导致名称冲突。因此,建议在需要时使用它并仅在局部作用域内使用(例如只在调用的时候使用)。
// 建议不在程序开头使用,而是在具体用到std库的函数内使用该语句
using namespace std;
4. 用全局函数要加命名空间
以下是两种建议的方式,或者用类,或者用 namespace
// 类的静态成员函数
class MyMath {
public:
static int add(int a, int b) {
return a + b;
}
static int sub(int a, int b) {
return a - b;
}
};
// or
// 命名空间内的非成员函数
namespace my_math {
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
}
5. 优先使用命名空间的非成员函数
能使用命名空间的非成员函数,就不用类的静态成员函数。
// 使用命名空间内的非成员函数
namespace my_math {
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
}
int main() {
int x = 3, y = 5;
int z = my_math::add(x, y); // 调用 my_math 命名空间中的函数
return 0;
}
// 不要使用类的静态成员函数模拟命名空间
class MyMath {
public:
static int add(int a, int b) {
return a + b;
}
static int sub(int a, int b) {
return a - b;
}
};
int main() {
int x = 3, y = 5;
int z = MyMath::add(x, y); // 调用 MyMath 静态方法
return 0;
}
六、其他 C++ 特性
1. 每个代码文件不超过 500 行
太长的代码阅读理解和维护的成本都太高,因此不建议一个文件太长。如果有文件超过 500 行,建议重构,把对象进一步的划分。
2. 禁止用 C++11 之后版本的语法
目前的主要考虑是用新语法会使得编译器编译成功的概率降低,另外提高开发者的开发门槛。因此,我们规定不能使用 C++11 之后的语法。
3. 多用前置自增 (++i)
对于迭代器和其他模板对象使用前缀形式 (++i) 的自增,自减运算符。
不考虑返回值的话,前置自增 (++i) 通常要比后置自增 (i++) 效率更高。
4. 尽可能用 sizeof(a) 代替 sizeof(int)
这里 a 是一个参数名
原因:当代码中变量类型改变时会自动更新。
5. 多使用列表初始化
C++11 中,该特性得到进一步的推广,任何对象类型都可以被列表初始化。示范如下:
// Vector 接收了一个初始化列表。
// 不考虑细节上的微妙差别,大致上相同。
// 可以任选其一。
vector<string> v{"foo", "bar"};
vector<string> v = {"foo", "bar"};
// 可以配合 new 一起用。
auto p = new vector<string>{"foo", "bar"};
// map 接收了一些 pair, 列表初始化大显神威。
map<int, string> m = {{1, "one"}, {2, "2"}};
// 初始化列表也可以用在返回类型上的隐式转换。
vector<int> test_function() { return {1, 2, 3}; }
// 初始化列表可迭代。
for (int i : {-1, -2, -3}) {}
// 在函数调用里用列表初始化。
void TestFunction2(vector<int> v) {}
TestFunction2({1, 2, 3});
6. 初始化时:整数用0,实数用0.0,指针用nullptr,字符 (串) 用'\0'
C++11 引入了一个新的关键字 nullptr,用来表示空指针。相对于传统的 NULL 或 0,nullptr 更加明确、类型安全。使用 nullptr 可以避免一些潜在的编程错误,比如将整数值误传给函数,导致出现不可预期的行为。
因此,建议在 C++11 及以上的版本中使用 nullptr 来表示空指针。
7. 少用 auto,使用前需对 auto 有更全面的了解
auto 是 C++11 引入的关键字,它可以让编译器自动推导出变量的类型。之后,C++14 和 C++17 对 auto 的使用也有了一些扩展和改进。
- C++11:
auto只能用于定义局部变量,并且必须初始化。例如:
auto i = 42; // 推导出 i 的类型为 int
auto f = 3.14f; // 推导出 f 的类型为 float
auto s = "hello"; // 推导出 s 的类型为 const char*
- C++14:
auto可以用于定义函数返回值类型,使得函数定义更加简洁。例如:
auto add(int x, int y) { return x + y; } // 推导出返回类型为 int
auto divide(double x, double y) { return x / y; } // 推导出返回类型为 double
- C++17:
auto进一步扩展为auto和模板结合使用时,可以直接指定模板类型参数,从而实现更加灵活的类型推导。例如:
std::vector<int> v{1, 2, 3};
auto it = v.begin(); // 推导出 it 的类型为 std::vector<int>::iterator
auto [first, second] = std::make_pair(1, 3.14); // 使用结构化绑定和 auto 推导出 first 和 second 的类型
但是大家注意,在 abacus 中,我们只支持 C++11 的标准,C++14/17 语法是不接受的。
8. auto 和 for 的混合使用时注意事项
在 C++11 中,auto 和 for 循环的结合使用已成为一种常见的编程范式,它可以让代码更加简洁、易读,并且减少了手动指定类型的错误。
int arr[] = {1, 2, 3};
for (auto i : arr) // 相当于复制
{
std::cout << i << " ";
}
for (const auto& i : arr) // 常量引用
{
std::cout << i << " ";
}
for (auto& i : arr) // 能够实现对元素的直接修改。
{
i *= 2;
}
9. constexpr 替代宏定义和 const 常量
在 C++11 里,用 constexpr 来定义真正的常量,或实现常量初始化。(真正的常量在编译时和运行时都不变)
#define PI 3.14159 // PI 是一个宏定义常量,它不会进行类型检查,容易出错;
const double kE = 2.71828; // kE 是一个 const 常量,它不能用于编译期计算。
constexpr double kGravity = 9.8;
constexpr 可以替代宏定义和 const 常量的主要原因是:
- 类型安全:使用
constexpr定义的常量会进行类型检查,避免了宏定义可能带来的类型错误,同时也比const常量更加严格。 - 编译时计算:
constexpr声明的变量或函数在编译时就可以被求值,而不需要在运行时计算。这比宏定义和const常量更高效,尤其是在需要多次使用同一个值的情况下。 - 更好的可读性和可维护性:使用
constexpr可以使代码更加清晰易懂,减少了宏定义可能导致的代码混乱问题。同时,由于constexpr可以使用函数、类等 C++ 语言特性,因此更加灵活,对于复杂的计算也更容易维护和修改。
因此,在 C++11 及以上的版本中,建议使用 constexpr 来替代宏定义和 const 常量,以提高代码的可读性、可维护性和效率。
七、关于 ABACUS 中常用的关键词缩写
有些名字很长,我们希望尽量言简意赅的表达出一些关键词的意思。原则是一般 3-5 个字母的范围下尽量说清楚一个变量的含义。这些统一的命名会出现在函数名或者变量名里。
1. 两个字符
pw:代表plane wave平面波op:代表具有multi-device和multi-precision支持的算子(operator),和Operator模块含义不同
2. 三个字符
fft:快速傅里叶变换kpt:布里渊区kpoint的缩写nao:代表numerical atomic orbitals (nao经常用来表示number of atomic orbitals,不知道会不会混)orb:orbital,轨道hmt:代表hamilt或者hamiltonianpot:代表potentialchg:代表chargeden:代表density(电荷密度尽量都用chg)scf:代表自洽迭代self consistent fieldthr:代表thresholdtab:代表tablekin:代表kinetic,动能的cal:代表calculateopt:代表optimizegen:代表generate
3. 四个字符
iter:代表iterationinit:代表初始化initializaitonread:读入stru:代表structureveff:代表有效势vloc:代表局域势