注释(Comments)
注释对于保持代码可读性至关重要。以下规则描述了你应该在何处注释什么。但请记住:虽然注释非常重要,但最好的代码是自文档化的。给类型和变量起合理的名称比使用晦涩的名称然后通过注释来解释要好得多。
编写注释时,为你的受众编写:下一个需要理解你代码的贡献者。大方一些——下一个可能就是你自己!
注释风格
使用 // 或 /* */ 语法均可,只要你保持一致。
虽然两种语法都可以接受,但 //
更常见。在注释方式和使用的风格上保持一致。
文件注释
每个文件以许可证样板文字开头。
如果一个源文件(如 .h
文件)声明了多个面向用户的抽象(公共函数、相关类等),应包含一个描述这些抽象集合的注释。包含足够的细节,以便未来的作者知道什么不适合放在那里。然而,关于单个抽象的详细文档应与这些抽象放在一起,而不是在文件级别。
例如,如果你为 frobber.h 写了文件注释,你不需要在 frobber.cc 或
frobber_test.cc 中包含文件注释。另一方面,如果你在没有关联头文件的
registered_objects.cc 中编写了一组类,你必须在
registered_objects.cc 中包含文件注释。
法律声明和作者行
每个文件都应包含许可证样板文字。为项目使用的许可证选择适当的样板文字(例如,Apache 2.0、BSD、LGPL、GPL)。
如果你对带有作者行的文件进行了重大更改,请考虑删除作者行。新文件通常不应包含版权声明或作者行。
结构体和类注释
每个不明显的类或结构体声明都应有一个附带的注释,描述其用途和使用方式。
// 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 {
...
};类注释
类注释应向读者提供足够的信息,让他们知道如何以及何时使用该类,以及正确使用该类所需的任何额外考虑。记录类做出的同步假设(如果有的话)。如果类的实例可以被多个线程访问,请格外注意记录围绕多线程使用的规则和不变量。
类注释通常是放置一小段示例代码片段的好地方,展示类的简单而专注的用法。
当充分分离时(例如,.h 和 .cc
文件),描述类使用方式的注释应与其接口定义放在一起;关于类操作和实现的注释应伴随类方法的实现。
函数注释
声明注释描述函数的使用方式(当它不明显时);函数定义处的注释描述操作方式。
函数声明
几乎每个函数声明前面都应该有注释,描述函数的功能和使用方式。
仅当函数简单且明显时(例如,类的明显属性的简单访问器),才可以省略这些注释。私有方法和在
.cc 文件中声明的函数也不例外。函数注释应以隐含的主语*This
function(本函数)*来编写,并应以动词短语开头;例如,“Opens the
file”而不是”Open
the file”。一般来说,这些注释不描述函数如何执行其任务。相反,那应该留给函数定义中的注释。
在函数声明处的注释中应提及的内容类型:
- 输入和输出是什么。如果函数参数名使用反引号提供,则代码索引工具可能能够更好地展示文档。
- 对于类成员函数:对象是否在方法调用持续时间之后仍记住引用或指针参数。这对于构造函数的指针/引用参数相当常见。
- 对于每个指针参数,是否允许为空以及如果为空会发生什么。
- 对于每个输出或输入/输出参数,该参数所处的任何状态会发生什么(例如,状态是追加还是覆盖?)。
- 如果函数的使用方式有任何性能影响。
以下是一个示例:
// 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;但是,不要不必要地冗长或陈述完全显而易见的事情。
在记录函数覆盖时,重点关注覆盖本身的具体细节,而不是重复被覆盖函数的注释。在许多这样的情况下,覆盖不需要额外的文档,因此不需要注释。
在注释构造函数和析构函数时,请记住阅读你代码的人知道构造函数和析构函数的用途,因此仅仅说”销毁此对象”之类的注释是没有用的。记录构造函数对其参数做了什么(例如,如果它们获取了指针的所有权),以及析构函数做了什么清理。如果这是平凡的,就跳过注释。析构函数没有头部注释是很常见的。
函数定义
如果函数完成工作的方式有任何棘手的地方,函数定义应该有一个解释性注释。例如,在定义注释中,你可能描述你使用的任何编码技巧,给出你经历的步骤的概述,或解释为什么你选择以这种方式实现函数而不是使用可行的替代方案。例如,你可能提到为什么函数的前半部分必须获取锁,但后半部分不需要。
请注意,你不应该仅仅重复在 .h
文件或其他地方的函数声明中给出的注释。简要概述函数的功能是可以的,但注释的重点应该是它是如何做的。
变量注释
一般来说,变量的实际名称应该足够描述性,以便很好地了解变量的用途。在某些情况下,需要更多注释。
类数据成员
每个类数据成员(也称为实例变量或成员变量)的用途必须清楚。如果有任何不变量(特殊值、成员之间的关系、生命周期要求)未被类型和名称清楚表达,则必须加注释。然而,如果类型和名称已经足够(int num_events_;),则不需要注释。
特别是,当哨兵值(如 nullptr 或 -1)不明显时,添加注释描述其存在和含义。例如:
private:
// Used to bounds-check table accesses. -1 means
// that we don't yet know how many entries the table has.
int num_total_entries_;全局变量
所有全局变量都应有注释描述它们是什么、用途是什么,以及(如果不清楚)为什么需要是全局的。例如:
// The total number of test cases that we run through in this regression test.
const int kNumTestCases = 6;实现注释
在你的实现中,你应该在代码中棘手的、不明显的、有趣的或重要的部分添加注释。
解释性注释
棘手或复杂的代码块前面应该有注释。
函数参数注释
当函数参数的含义不明显时,考虑以下补救措施之一:
- 如果参数是字面常量,且相同的常量在多个函数调用中以默认相同的方式使用,你应该使用命名常量来使该约束显式化,并保证它成立。
- 考虑更改函数签名,用
enum参数替换bool参数。这将使参数值自描述。 - 对于有多个配置选项的函数,考虑定义一个类或结构体来保存所有选项,并传递该实例。这种方法有几个优点。选项在调用点按名称引用,这澄清了它们的含义。它还减少了函数参数数量,使函数调用更容易阅读和编写。作为额外的好处,当你添加另一个选项时不必更改调用点。
- 用命名变量替换大型或复杂的嵌套表达式。
- 作为最后手段,使用注释在调用点澄清参数含义。
考虑以下示例:
// What are these arguments?
const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);versus:
ProductOptions options;
options.set_precision_decimals(7);
options.set_use_cache(ProductOptions::kDontUseCache);
const DecimalNumber product =
CalculateProduct(values, options, /*completion_callback=*/nullptr);不要做的事
不要陈述显而易见的事情。特别是,不要直接描述代码做了什么,除非对于精通 C++ 的读者来说行为不明显。相反,提供描述代码为什么这样做的更高层次注释,或者使代码自描述。
比较这个:
// Find the element in the vector. <-- Bad: obvious!
if (std::find(v.begin(), v.end(), element) != v.end()) {
Process(element);
}和这个:
// Process "element" unless it was already processed.
if (std::find(v.begin(), v.end(), element) != v.end()) {
Process(element);
}自描述的代码不需要注释。上面示例中的注释就是显而易见的:
if (!IsAlreadyProcessed(element)) {
Process(element);
}标点、拼写和语法
注意标点、拼写和语法;阅读写得好的注释比阅读写得差的注释更容易。
注释应该像叙述文本一样可读,具有正确的大写和标点。在许多情况下,完整的句子比句子片段更具可读性。较短的注释,例如代码行末尾的注释,有时可以不那么正式,但你应该与你的风格保持一致。
虽然代码审查者指出你在应该使用分号的地方使用了逗号可能令人沮丧,但源代码保持高水平的清晰度和可读性非常重要。正确的标点、拼写和语法有助于实现这一目标。
TODO 注释
对临时的、短期解决方案的或足够好但不完美的代码使用 TODO 注释。
TODO 应包含全大写的 TODO 字符串,后跟错误 ID、姓名、电子邮件地址或与
TODO 引用的问题具有最佳上下文的人员或问题的其他标识符。
推荐的风格(按优先顺序排列):
// TODO: bug 12345678 - Remove this after the 2047q4 compatibility window expires.
// TODO: example.com/my-design-doc - Manually fix up this code the next time it's touched.
// TODO(bug 12345678): Update this list after the Foo service is turned down.
// TODO(John): Use a "\*" here for concatenation operator.如果你的 TODO
是”在未来某个日期做某事”的形式,请确保包含非常具体的日期(“在 2005 年 11
月之前修复”)或非常具体的事件(“当所有客户端都能处理 XML
响应时移除此代码。”)。