作者:韩昊知,邮箱:[email protected]
审核:陈默涵,邮箱:[email protected]
最后更新时间:2023/09/04
ABACUS(Atomic-orbtial Based Ab-initio Computation at UStc,中文名原子算筹)是国产开源密度泛函理论软件,相关介绍 ABACUS 的新闻可在 ABACUS 新闻稿整理查看。此文档用于给 ABACUS 开发者提供代码编程规范方面的建议。
让任何开发者都可以快速读懂别人的代码,这点对于 ABACUS 项目很重要。此外,使代码易于管理的方法之一是加强代码一致性,这需要从代码规范开始着手。
以下是四种命名方法:下划线命名法、匈牙利命名法、驼峰式命名法、帕斯卡命名法
<strong>string table_name; </strong>// 下划线命名法(推荐) - 用下划线区分单词(推荐使用)
string sTableName; // 匈牙利命名法 - 前缀字母用变量类型缩写,单词首字母大写
string tableName; // 驼峰命名法 - 混合大小写(不推荐使用)
string TableName; // 帕斯卡命名法 - 每个单词首字母大写(不推荐使用)
下划线命名法:推荐使用
匈牙利命名法:目前 ABACUS 中有些变量(例如指针),首字母为数据类型(指针为 p),可以考虑用这种方式命名,但不推荐用大写字母
驼峰命名法/帕斯卡命名法:目前 ABACUS 大部分的代码不是用这种命名方式的,为了代码风格统一,所以不推荐使用
建议使用下划线命名法且所有字母全部小写,建议小于 18 个字符
void calculate_area() {
// function body
}
private
的类数据成员和普通变量的命名方式一样,但要在最后以下划线结尾,以区分自己是 private
的类数据成员。
class TableInfo
{
...
private:
string table_name_; // 好 - 后加下划线
string tablename_; // 好
static Pool<TableInfo>* pool_; // 好
};
在 ABACUS 中,例如 NBANDS(能带数),NLOCAL(局域轨道数目)这种全大写的变量名是全局变量,其它的不建议用大写字母给变量命名。另外,ABACUS 将通过重构或者删除的方式逐步淘汰全局变量,因此不建议增加新的全局变量。
不需要的 #include
删除。
项目内头文件应按照项目源代码目录的树结构排列,避免使用 UNIX 特殊的快捷目录 .
(当前目录) 或 ..
(上级目录)。例如
source/module_hsolver/diago_cg.h
文件是这样引用 module_base
的头文件:
#include "diagh.h"
#include "module_base/complexmatrix.h"
使用标准的头文件包含顺序,这样可以可增强可读性,避免隐藏依赖。
foo.cpp
中包含头文件的次序如下:
- 与源文件对应的头文件:
dir2/foo2.h
(这种优先的顺序排序保证当dir2/foo2.h
遗漏某些必要的库时,dir/foo.cpp
的构建会立刻中止)- C 系统文件
- C++ 系统文件
- 其他库的
.h
文件- 本项目内
.h
文件
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_
使用 #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
是一个存在的类,但不提供该类的详细信息。
只有当函数只有 10 行甚至更少时才将其定义为内联函数(关键字 inline
)。
若要使用,建议提交 issue 讨论
只有数据成员、没有成员函数时可以用 struct
建议使用 C++ 的类型转换,如 static_cast<>()
。
不要使用 int y = (int)x
或 int y = int(x)
等转换方式。
- 用
static_cast
替代 C 风格的值转换, 或某个类指针需要明确的向上转换为父类指针时。- 用
const_cast
去掉const
限定符。- 用
reinterpret_cast
指针类型和整型或其它指针之间进行不安全的相互转换。 仅在你对所做一切了然于心时使用。
如果你想使用私有继承,你应该替换成把基类的实例作为成员对象的方式去定义类。
例如,如下案例:
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;
}
函数的输入参数与输出参数:
在一个函数中,不变的量,我们可以看作是函数的输入参数;变化的量,我们可以看作是函数的输出参数。
在输入参数中可以选择 const T*
(指向常量对象的指针,不能通过这个指针来修改其指向的对象的值。然而,你可以改变指针本身的值)。
也可以使用 const T&
(不能通过这个引用来修改其引用的对象的值,在其生命周期内不能重新引用另一个对象)。
所以,建议使用
const T&
,若要使用const T*
,则应给出相应的理由,否则会使读者感到迷惑。
void Foo(const string &in, string *out);
// 输入参数:in (const + 引用&)
// 输出参数:out(指针变量)
什么是引用参数:
- 在 C 中, 如果函数需要修改输入变量的值, 参数必须为指针, 如
int foo(int *pval)
。 - 在 C++ 中, 函数还可以声明为引用参数:
int foo(int &val)
。
引用参数在语法上是值变量却拥有指针的语义(变量可以被改变!)。
建议编写简短,凝练的函数,有特殊情况的除外。
无初始化的变量可能会引起结果不稳定(例如出现随机数),因此建议养成习惯,对所有变量的值要初始化,见下面的例子:
int i;
i = f(); // 坏——初始化和声明分离
int j = f(); // 好——初始化时声明
vector<int> v; // 坏——初始化和声明分离
v.push_back(1);
v.push_back(2);
vector<int> v = {1, 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); }
使用 using namespace
语句来引入命名空间中的所有名称可能会导致名称冲突。因此,建议在需要时使用它并仅在局部作用域内使用(例如只在调用的时候使用)。
// 建议不在程序开头使用,而是在具体用到std库的函数内使用该语句
using namespace std;
以下是两种建议的方式,或者用类,或者用 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;
}
}
能使用命名空间的非成员函数,就不用类的静态成员函数。
// 使用命名空间内的非成员函数
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;
}
太长的代码阅读理解和维护的成本都太高,因此不建议一个文件太长。如果有文件超过 500 行,建议重构,把对象进一步的划分。
目前的主要考虑是用新语法会使得编译器编译成功的概率降低,另外提高开发者的开发门槛。因此,我们规定不能使用 C++11 之后的语法。
对于迭代器和其他模板对象使用前缀形式 (++i
) 的自增,自减运算符。
不考虑返回值的话,前置自增 (++i
) 通常要比后置自增 (i++
) 效率更高。
这里 a
是一个参数名
原因:当代码中变量类型改变时会自动更新。
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});
C++11 引入了一个新的关键字 nullptr
,用来表示空指针。相对于传统的 NULL
或 0
,nullptr
更加明确、类型安全。使用 nullptr
可以避免一些潜在的编程错误,比如将整数值误传给函数,导致出现不可预期的行为。
因此,建议在 C++11 及以上的版本中使用 nullptr
来表示空指针。
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 语法是不接受的。
在 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;
}
在 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
常量,以提高代码的可读性、可维护性和效率。
有些名字很长,我们希望尽量言简意赅的表达出一些关键词的意思。原则是一般 3-5 个字母的范围下尽量说清楚一个变量的含义。这些统一的命名会出现在函数名或者变量名里。
pw
:代表plane wave平面波op
:代表具有multi-device和multi-precision支持的算子(operator),和Operator模块含义不同
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
iter
:代表iterationinit
:代表初始化initializaitonread
:读入stru
:代表structureveff
:代表有效势vloc
:代表局域势