本文旨在为HHVM编写C++代码提供一种指南,包括了什么时候、怎么使用各种语言功能,以及代码的格式。我们的目标是确保代码持续高可用的同时,还能容易被阅读和参与贡献,尤其是对新人而言。
HHVM代码库包含来自很多不同作者的大量代码。它已经经历了多个不同的重要阶段,包括存在于多个不同的仓库中。结果就是,有很大一部分代码(主要是老代码)不符合这个指南。所以在疑惑怎么写代码,或者怎么格式代码时,总是要优先考虑本文中的建议,而不要去参考已有代码的例子。如果你已经在工作中接触了一些老代码,请在过程中将它们清理干净。但不要为了让代码符合规范,就去花时间修改你不需要改的代码。我们当然喜欢整个代码库都符合规范,但希望逐渐到达这个目标,不因为一些纯粹的外观上的修改而损失掉git历史和开发时间。也就是说,如果你在做的外观上的修改的量很大,且范围在不断增长,你可能更应该拉出一个单独分支再修改。
上面说的原则并没有一个明确的分界——只是试图最小化reviewer需要的工作。一个好方法是,如果你的外观改动需要在diff中增加重要的新部分(例如重命名一个函数需要修改所有调用点),它可能就需要单独拉出一个分支。
头文件
HHVM仓库中每个.cpp都需要一个对应的同名.h,里面声明了它的public接口。我们倾向于把API文档看的比实现代码中内联的注释更重要,所以所有头文件中的声明(类、枚举、函数、常量,等等)都应该文档化。更详细内容见注释和文档
。
构建时间是很多大型C++项目的痛点。不要搞一个很大的头文件,里面include了一组其它的大型头文件。这会阻止“引用你需要的”,见如何include
。
include守卫
为了防止多次include,所有头文件都要有define守卫,长这样:
#ifndef incl_HPHP_<OPTIONAL-DIR>_<FILE>_H_
#define incl_HPHP_<OPTIONAL-DIR>_<FILE>_H_
// Lots of nice codez.
#endif
OPTIONAL_DIR可以是像JIT
这样的东西,但不需要包含路径中比较没意思的部分,像是RUNTIME
或BASE
。只要保证整个代码库中唯一就好。
如何include
如何include的黄金原则就是:引用你需要的(IWYU)。简单说,你不能依赖你引用的其它头文件带来的间接引用。你还应该优先考虑前向声明那些你不需要它的定义的类和结构(所谓“不引用你不需要的”),有助于降低HHVM的编译时间。
为了让IWYU更容易实现,我们有以下include原则:
- 总是第一个引用.cpp对应的.h
- 将include分组:C++标准库、第三方库、最后是HHVM的头文件。为了可读性,组与组之间要有一个空行。(要不要把HHVM的头文件按子系统(如
JIT
)分隔开留给作者考虑) - 组内各文件按字典序排列。这有助于确保所有必需的头文件都被引入了,且后面没有多余的头文件。
- Folly和HHVM的头文件用“""”,其它头文件用“<>”。
一个例子是bytecode.cpp
的引用部分:
#include "hphp/runtime/vm/bytecode.h"
#include <cstdio>
#include <string>
#include <boost/program_options/options_description.hpp>
#include "hphp/runtime/vm/class.h"
#include "hphp/runtime/vm/func.h"
#include "hphp/runtime/vm/hhbc.h"
#include "hphp/util/string.h"
内联函数
我们鼓励将非常短的函数定义为内联函数。
当定义class或struct的内联的成员函数时,如果类的接口很简单、很紧凑(例如智能指针类或是包装类),为了简洁,优先选择将函数放到类的定义内。
但对于那些有着更复杂,未来容易激增大量的inline helper API的类时,要限制在类的定义中只包含成员函数的原型。这让API干净不少。对于这些类,将所有inline函数都放到对应的-inl.h
中。
// At the bottom of func.h.
#include "hphp/runtime/vm/func-inl.h"
// After the copyright in func-inl.h.
namespace HPHP {
// Definitions go here.
}
对于API大到需要用-inc.h的类,要将所有的定义都放到-inc.h里,即使是只有一行的getter/setter。这样既能保持干净的API,还能避免实现代码被分在三个文件里(.h、-inc.h、.cpp)。
有一些文件——可能有对应的-inc.h,也可能没有——可能需要一个-defs.h。这个文件里也包含inline函数的实现,但它不需要被include到.h中。它只被一部分需要使用这些定义的地方include,或是这些定义因为循环依赖而无法放到.h中时。那些确实需要使用这些定义的地方需要直接include对应的-def.h。
结构和类
HHVM代码库中广泛使用了类,有着大量的代码规范。在类的命名方面同样参见命名
。
用struct还是class
C++中struct
和class
有着差不多的语义,唯一的区别就在于默认的访问权限(struct
默认public而class
默认private)。
我们不对这两个关键字赋予更多的含义,所以我们在所有地方都用struct
。为了在MSVC下编译,我们也需要在类的定义和前向声明中只用struc/class的一个,因为MSVC没有遵守C++规范,而在所有地方坚持用struct
会让编译容易一些。
访问控制
尽量避免使用protected
,它给一种封装上的错误的安全感:既然每个人都可以继承自你的类,那每个人也都可以轻易访问到protected
成员。
隐式和显示构造器
单参数且参数不是initializer_list的构造器默认都要用explicit
。
struct MyStruct {
// We don't want to implicitly convert ints to MyStructs
explicit MyStruct(int foo);
// Two-argument constructor; no need for explicit
MyStruct(const std::string& name, int age);
};
用public数据成员还是getter/setter
优先声明public成员,而不是使用getter/setter。不需要用特殊方式管理对象的getter和setter会导致API的膨胀及不需要的引入。
当然,对于private成员的访问还是鼓励使用getter,但不要在函数名前面加get
:
struct Func {
const SVInfoVec& staticVars() const;
void setStaticVars(const SVInfoVec&);
BuiltinFunction builtinFuncPtr() const;
static constexpr ptrdiff_t sharedBaseOff();
};
声明顺序
在struct和class定义中坚持以下的声明顺序:
- 友元类。
- 嵌套类、枚举、typedef。(如果可能,在这里只声明嵌套类,把它的定义放到类的定义后面)
- 构造器,析构器。
- 成员函数,包括static函数,文档化,按逻辑分组。
- 常量和static数据成员。
- 所有实例数据成员,无论可访问性是什么。
private成员函数可以散在public函数中,也可以降到数据成员前的一个单独区域。但所有实例属性必须连续出现在类定义的最后。
其它C++功能
没几个语言特性被无条件的禁止了。但如果你想用像goto
或是operator,()
这样有争议的功能,你最好有令人信服的证据说明它为什么比其它替代品要好。C++是一种非常大,非常复杂的语言,我们不想人为的限制开发者能做的事,但这就要把很多责任放到你的肩上。
我们自己维护一个风格指南,而不是采用一个已有的指南,一个主要的驱动因素就是避免限制有用的语言特性(例如:异常、模板、lambda)。
命名空间
所有HHVM的代码都要放到namespace HPHP { /* everything */ }
的范围里。大的子模块,像是HPHP::jit
和HPHP::rds
可以放到HPHP
内的一个单独命名空间里。我们通常用匿名命名空间代替static
来维护编译单元内部的符号。这主要取决于作者,但要注意:不像函数和变量,类和结构为了被正确的隐藏起来,必须放到匿名命名空间里。
枚举
在任何可能的场合使用enum class
。只有在你期望你的枚举类型会被频繁当作整数使用时,例如数组索引,你才可以使用旧风格的枚举语法。
命名
HHVM代码遵守多种命名规范。
在规范没有明确定义的场合,通常选择你正在修改的文件的已有规范——例如,一个结构的数据成员的名字都是m_namesLikeThis
,那就选择m_namesLikeThis
而不是m_this_style
,即使代码库的其它地方都在用后者。
变量
用lowerCamelCase
或lower_case_with_underscores
来命名所有本地变量,看文件的已有风格是哪种就用哪种。静态变量(无论是定义在匿名命名空间里的还是用static
修饰的)都应该加上前缀s_
(例如s_funcVec
)。全局变量也类似,前面加上g_
(例如g_context
)。
常量
所有常量都要加上前缀k
并使用CamelCase
,例如kInvalidHandle
。在可以用constexpr
的场合就要用constexpr
而不是const
。
类数据成员
和变量一样,所有数据成员都要用lowerCamelCase
或lower_case_with_underscores
。另外,私有的实例成员要有前缀m_
(例如m_cls
或m_baseCls
或m_base_cls
),静态成员要有前缀s_
(例如s_instance
)。我们倾向于不给public成员加前缀。
函数
我们通常用lowerCamelCase
命名那些暴露在头文件里的函数,包括成员函数。当然我们也使用lower_case_with_underscores
(例如hphp_session_init
),主要用在实现文件内部。一般来说参照你正在修改的文件的已有规范就好。
如果你在模仿一个已有模式的类,例如STL容器类,最好遵循对应的规范(例如my_list::pusk_back
要比my_list::pushBack
更推荐使用)。
类
类名用UpperCamelCase
,除了那些模仿STL容器或智能指针风格的类。
命名空间
新的命名空间应该用lowercase
——我们强烈推荐在一般场合用一个单词的命名空间。对于更长的命名空间(例如vasm_detail
),用lower_case_with_underscores
。
其它规范
推荐在新代码中使用正确的首字母大写形式(例如选择IRTranslator
而不是HhbcTranslator
)。这种方式下,新代码优先用ID
而不是Id
。
格式
一致的代码格式并不直接影响正确性,但它让代码的阅读和维护变得简单。因此,我们定义了一组规范,来指导如何格式化代码。很可能我们制订的规则会与你自己的个人风格有冲突,但我们相信让代码库保持前后一致,易于阅读,要比每个开发者坚持自己的审美更重要。
这里没有规定的地方都留给开发者决定。但本文不是一成不变的,所以如果有新的特定格式在代码审核中出现,我们可能要把它添加进来。
一般规则
- 所有缩进都要用空格表示。
- 每级缩进2个空格。
- 每行不得超过80个字符,除非某种语法绝对需要这么做。
- 行尾不能有任何空格,包括缩进不为0的空行;这些行只能有换行符。
类型和变量
- 声明变量或用typedef时,
*
和&
要放到类型一侧,而不是名字一侧(如const Func*& func
)。 - 变量声明要限制在一行内。
函数签名
下面的函数签名格式都是可以的:
// If arguments would fit on 1 line:
inline void Func::appendParam(bool ref, const Func::ParamInfo& info) {
}
// If the arguments need to wrap, we have two accepted styles, both of which
// are OK even if the wrapping wasn't necessary:
SSATmp* HhbcTranslator::ldClsPropAddr(Block* catchBlock,
SSATmp* ssaCls,
SSATmp* ssaName,
bool raise) {
doSomeStuff();
}
// This style is helpful if any of the function, argument, or type names
// involved are particularly long.
SSATmp* HhbcTranslator::ldClsPropAddr(
Block* catchBlock,
SSATmp* ssaCls,
SSATmp* ssaName,
bool raise
) {
doSomeStuff();
}
要始终保持返回类型和函数名在同一行,除非留给参数的空间不够用了。其它修饰的关键字也要放到同一行(inline
、static
等等)。
换行的参数要保持缩进相同。左大括号要与最后一个参数同一行,除了类的构造器(见"构造器初始化列表"一节)。在头文件里写函数声明时,要包括参数名,除非这个参数不需要名字也足够表达含义:
struct Person {
// The single string argument here is obviously the name.
void setName(const std::string&);
// Two string arguments, so it's not obvious what each one is without names.
void setFavorites(const std::string& color, const std::string& animal);
};
语句
条件和循环语句应该用这样的格式:
if (vmpc() == nullptr) {
fprintf(stderr, "whoops!\n");
std::abort();
}
注意在if
后面有一个空格,而在条件与两边的括号之间没有空格,在右括号与左大括号之间有一个空格。所有代码块中的部分都要比if
多一级缩进。如果整个语句(条件和代码块)可以放到一行内,那就可以放到一行里,不要大括号。其它情况下大括号都是必需的。例如,下面这个可以:
if (obj->_count == 0) deleteObject(obj);
for (auto block : blocks) block->setParent(nullptr);
但下面这些不可以:
if (veryLongVariableName.hasVeryLongFieldName() &&
(rand() % 5) == 0) launchRocket();
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
避免在条件表达式中使用赋值,除非目标变量就是在这个条件中声明的,例如:
if (auto const unit = getMyUnit(from, these, args)) {
// Do stuff with unit.
}
优先用C++11中的foreach语法来做显式的迭代:
for (auto const& thing : thingVec) {
// Do stuff with thing.
}
表达式
所有二元运算符两边都要有一个空格,除了
.
、->
、.*
和->*
,两边不要空格。不要加多余的括号,除非你觉得不加会影响可读性。一个好方法是如果你或你的reviewer需要查运算符优先级才能正确解释表达式,你就可能需要加括号了。这种情况下GCC或clang可能会建议你加括号;我们编译时都会加上
-Werror
这样保证遵循这些指南。-
如果一个表达式一行放不下,那么分成多行,每行最后是运算符,两个相同等级的表达式的缩进要对齐。例如,下面是几个被适当格式化的长表达式:
if (RuntimeOption::EvalJitRegionSelector != "" &&
(RuntimeOption::EvalHHIRRefcountOpts ||
RuntimeOption::EvalHHITExtraOptPass) &&
Func::numLoadedFuncs() < 600) {
// ...
} longFunctionName(argumentTheFirst,
argumentTheSecond,
argumentTheThird,
argumentTheFourth); -
函数调用应该主要遵循第一种格式。如果函数的一个或多个参数很宽,你可能需要将每个参数单独放一行,保持它们的缩进相同,比当前范围深一级。这个原则一直有效,但尤其常见于传递lambda时:
m_irb->ifThen(
[&](Block* taken) {
gen(CheckType, Type::Int, taken, src);
},
[&] {
doSomeStuff();
lotsOfNonTrivialCode();
// etc...
}
);
构造器初始化列表
如果初始化列表可以放到一行里,就可以这么做:
MyClass::MyClass(uint64_t idx) : m_idx(idx) {}
MyClass::MyClass(const Func* func) : m_idx(-1) {
// Do stuff.
}
否则,下面这么做总是正确的:
MyClass::MyClass(const Class* cls, const Func* func, const Class* ctx)
: m_cls(cls)
, m_func(func)
, m_ctx(ctx)
, m_isMyConditionMet(false)
{}
MyClass::MyClass(const Class* cls, const Func* func)
: m_cls(cls)
, m_func(func)
, m_ctx(nullptr)
, m_isMyConditionMet(false)
{
// Do stuff.
}
命名空间
我们不会搞多层嵌套的命名空间,所以推荐将所有层次的命名空间写在一行内:
namespace HPHP { namespace jit { namespace x64 {
///////////////////////////////////////////////////////////////////////////////
/*
* Some nice documentation.
*/
struct SomeNiceThing {
// some nice properties
};
///////////////////////////////////////////////////////////////////////////////
}}}
命名空间内不要增加缩进。相反,为了更突出命名空间,考虑在其头尾加上一行'/'(对于匿名命名空间尤为有效)。我们推荐这种展示形式,但不严格要求它的格式(70、79、80字符都可以,在它与大括号间有没有空行也都可以)。
注释
所有头文件里公开和私有的API都要有详细的文档。不显而易见的名字和记号(例如'persistent'或'simple')要有说明。前置条件和后置条件也要说明。
对于复杂逻辑,推荐行内注释,但密度取决于作者。你的注释不应该只是代码的总结/释义,是要专注于解释这段代码的首要任务是什么以及这个任务为什么重要或值得去做。
注释风格
以下是我们使用或避免的一些注释风格:
// This style of comment is the most common for relatively short inline
// comments. It's fine if it's multi-line.
//
// It's also fine if it has line breaks. The extra newline aids readability in
// this case.
/*
* This style of comment is the right one to use for struct/function
* documentation. Prefer one star on the opening line, as opposed to the
* doxygen standard of two.
*
* This is also sometimes used for inline code comments, although the // style
* makes it easier to comment out blocks of code.
*/
struct ClassLikeThing {
std::vector<const Func*> methods; // This is fine for short annotations.
/* This is also ok, though try not to mix and match too much in a file. */
std::vector<const ClassLikeThing*> parents;
};
/* Don't write multiline comments where some lines are missing their prefix.
This is pretty weird. */
试着在除了最短的注释之外的注释中使用完整的句子。所有注释的宽度都要控制在79个字符内。
分隔符
用一行'/'来分隔代码段。这块没有严格的规范,但推荐用一行'/'而不是其它形式(例如/*****/
,5个空行,或是ASCII图画)。
版权信息
所有文件都必须以HHVM的版权声明开头:
/*
+----------------------------------------------------------------------+
| HipHop for PHP |
+----------------------------------------------------------------------+
| Copyright (c) 2010-201x Facebook, Inc. (http://www.facebook.com) |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| http://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/
// File contents start here.