代码风格
使用ChatGPT翻译
编码标准
编码标准
最新的 JUCE 编码标准。
JUCE 代码库具有非常严格和一致的编码风格。这种风格多年来逐渐发展,融合了公认的最佳实践 C++ 建议、经验和个人偏好。 为了响应要求制定代码库遵循的规则集的要求,我们尝试列出一些我们认为最重要的指导原则。 其中一些规则被普遍接受为基本的 C++ 良好实践。其中一些是 JUCE 特有的。很多只是个人品味!我们并不是说这是一套您应该遵循的权威规则,我们也不是特别想争论或捍卫这里的任何观点——这只是对多年编码后事情最终结果的描述。YMMV!
不要重复自己!
这一原则几乎概括了编写优质代码的精髓,无论使用何种语言,何种水平。其他人对此的解释比我们在这里做的更好——在 Google 上搜索 DRY,您会找到许多教程。如果您只关注本文档中的一条建议,那就选择这条吧!
表面的东西——布局和空白
以下规则不会对代码的实际功能产生任何影响,但美观性和一致性对于使代码易于理解非常重要。
没有制表符!
制表符是 4 个空格!
括号按Allman 风格缩进 。
所有二元运算符前后务必加空格!(声明运算符时除外,例如
bool operator== (Foo)
)
操作
!
符后面应该始终有一个空格,例如if (! foo)
运算
~
符前面应该有一个空格,但后面不能有空格。与 运算符之间不应有空格
++
。--
切勿在逗号前加空格。
逗号后务必加一个空格。
始终在包含文本的左括号前留一个空格 - 例如
foo (123);
切勿在空的开/闭括号前放置空格 - 例如
foo();
当用作数组索引时,不要在方括号前放置空格 - 例如
foo[1]
如果 、 、 、 语句前面有另一个语句,则在
if
、for
、while
、 语句前留一个空行 。例如do
一般来说,在右括号后留一个空行
}
(除非下一行只是另一个右括号)不要把
if
所有语句都写在一行上……...除非!如果你有一组连续的 if 语句,并且它们相似,通过垂直对齐它们,可以清楚地看到相似点和不同点的模式,例如
有些人要求在 if 语句周围使用括号,即使是非常短、简单的语句。我们的规则是,对于非常简单的单行语句,括号会增加视觉混乱,而不会使事情变得更清晰,因此省略括号。但是,如果有多行,或者涉及的表达式很长,请添加括号。
在具有多个分支的 if-else 语句中,所有分支都应采用相同的格式,即,要么全部使用括号,要么都不使用括号。
Lambdas:以下是首选样式的示例:
当编写指针或引用类型时,我们总是在它后面加一个空格,而不是在它前面加一个空格。例如
是的 - 我们知道很多人会认为这种声明在技术上更正确的布局应该是:
但我们认为将星号放在类型名称上更有意义,因为指针属性是属于类型的特性,而不是变量的特性。唯一可能导致混淆的情况是,在同一个语句中声明多个相同类型的指针时——这引出了下一条规则……
声明涉及星号或与号字符的多个指针或同一类型的引用时,切勿在单个语句中这样做,例如
而是将它们分成不同的行并再次写出类型名称,这样可以清楚地说明发生了什么,并避免遗漏任何重要星号的危险。
或者更好的是,使用智能指针或 typedef 来创建不需要星号或与号的类型名称!
我们在类型名称前放置'const'修饰符,即
两者的作用相同,但前者更接近于用英语口头描述类型的方式,即先动词后名词的顺序。有人支持使用尾随 const,因为它可以让非常复杂的类型名称变得更容易,但如果你的类型名称包含多个级别的 const 修饰符关键字,那么你可能应该使用 typedef 将其简化为更易于管理的类型名称。
模板参数应该遵循其类型,不带空格,例如
vector<int>
...但在模板语句中,我们确实在开括号前留了一个空格,例如
当将包含运算符(或点运算符)的表达式拆分到多行时,每行都应以运算符符号开头,例如
长行...许多编码标准强制将行的最大长度限制为 80 个字符左右,并禁止任何例外。显然,保持行短而清晰是一条很好的一般规则 - 我们不想让读者过于频繁地使用水平滚动条。但是,在许多情况下,严格的行长限制会产生令人困惑的缩进,长表达式必须尴尬地分成多行,或向后移动几个空格才能使行尾保持在限制以下。在这些情况下,较长的行可以大大提高可读性并有助于突出显示错误。例如,如果您有一系列相似但略有不同的语句,可以通过将每个语句放在一行上使其垂直排列,这会使垂直相似性脱颖而出,并使代码所做操作的整体模式在视觉上显而易见。
对于一两行的简短注释,尽可能使用
//
而不是/* */
,因为这使得在调试时注释掉大块代码变得更容易。//
单行注释中的文本前务必留一个空格
多行注释在左侧垂直对齐,例如
十六进制通常用小写字母表示,例如
0xabcdef
浮点文字:我们总是在点之前和之后添加至少一个数字,例如
字符串连接:请避免过度使用 typename
String
!出于某种原因,我似乎经常在人们的代码中看到这种膨胀,但无法理解原因。例如
命名约定
变量和方法名称采用驼峰命名法,并且始终以小写字母开头,例如
myVariableName
类名也采用驼峰式命名法,但始终以大写字母开头,例如
MyClassName
不允许使用匈牙利表示法 。互联网上已经有很多支持/反对这种风格的论据,所以我们不会在这里加入争论。但大多数其他编码标准,包括标准库本身,已经不再将类型和名称混合在一起。类型可能会因模板或使用“auto”而改变,名称应该反映变量的用途,而不是其类型。
我们倾向于避免在变量名中使用下划线,除非有必要将长而难以阅读的名称转换为更易读的名称。
变量名中绝对不允许使用前下划线或尾下划线!前下划线在标准库代码中具有特殊地位,因此在使用代码中使用它们会显得非常不协调。
如果您确实需要编写宏,则必须是 ALL_CAPS_WITH_UNDERSCORES。由于它们是唯一以全大写形式书写的符号,因此很容易识别它们。
由于宏没有命名空间,因此必须保证它们的名称不会与其他库或第三方代码中使用的宏或符号冲突,因此您应该使用项目独有的名称开头。所有 JUCE 宏都以 开头
JUCE_
。对于枚举,请使用与类及其成员变量相同的大小写驼峰式命名法,例如
编写模板时,请避免使用
T
或其他单字符模板参数。给它们起一个有用的名字并不需要花费太多精力,而且T
可能会与某些粗心的第三方标头中定义的宏发生冲突。
类型、const 正确性等
如果某个方法可以(并且应该!)是 const,就将其设为 const!Herb Sutter 对 const 在类的线程安全方面的含义有有趣的看法,值得一 读。
如果在子类中重写虚方法,请始终用说明
override
符标记它,并且切勿为其添加多余的virtual
关键字!如果某个方法确实不会引发异常(请小心!),请将其标记为
noexcept
。尽可能在所有情况下都这样做,因为在某些情况下,这可能会对性能产生巨大影响 - 在极端情况下,性能可能会提高 10 倍。当返回临时对象(例如 String)时,返回的对象应该是非 const 的,以便如果该类具有移动运算符,则编译器可以使用它。
如果您有一个不会改变的局部变量或函数参数,那么您应该考虑将其设为 const。但这里要考虑的因素是,添加
const
会使代码更冗长,并且如果 const 性不重要或很明显,则可能会增加不必要的视觉混乱。或者,如果它有助于引起读者注意这个变量是常量,那么它可能是一件好事。一般规则是,在非常短的代码块中,不必担心将局部变量设为 const。在变量被多次使用的较长块中,如果您认为这对读者有用,您可能希望将其设为 const。请注意,在几乎所有情况下,将原始值声明为const
对编译器的代码生成都没有任何影响。如果某个东西是编译时常量,那么总是将其声明为 constexpr!
请记住,指针可以是 const 也可以是原语 - 例如,如果您有一个 char 指针,其内容将被更改,那么您仍然可以使指针本身成为 const,例如
char* const foobar = getFoobar();
。不要在函数或方法的顶部声明所有局部变量(即采用老式的 C 风格)。尽可能在最后时刻声明它们,以尽可能严格地限制它们的作用域。
当你测试一个指针是否为空时,永远不要写
if (myPointer)
。始终通过更完整地编写来避免隐式转换为布尔值:
同样,永远不要写
if (! myPointer)
,而要总是写
这里的原因是它更具可读性,因为它与您用英语读出表达式的方式相匹配 - 即“如果变量 myPointer 为空”。
避免使用 C 风格的强制类型转换,除非在明显是原始数字类型之间进行转换。有些人会说“完全避免使用 C 风格的强制类型转换”,但当您只想简单地将 int 转换为 float 时,编写 static_casts 可能会有点冗长。但只要涉及指针、模板或非原始类型,就始终使用现代强制类型转换。当您重新解释数据时,请始终使用 reinterpret_cast。
对于 64 位整数类型:在 JUCE 中我们声明 juce::int64(和其他类型),但这是在 C++11 引入 int64_t 之前添加的。我们鼓励使用其中任何一种,而不是
long long
。
对象生命周期和所有权
绝对不要使用
delete
、deleteAndZero
等。在极少数情况下不能使用智能指针或其他自动生命周期管理类。new
除非别无选择,否则 不要使用 。每当您输入 时new
,始终将其视为寻找更好解决方案的失败。如果可以在堆栈而不是堆上分配局部变量,那么请始终这样做。永远不要使用
new
或 malloc 来分配 C++ 数组。始终优先使用 juce::HeapBlock 或其他容器类。..只是为了更加清楚:您(几乎)根本不需要使用 malloc 或 calloc!
如果父类需要创建并拥有某种子对象,请始终使用组合作为首选。如果这不可能(例如,如果子类需要指向父类的指针作为其构造函数),则使用 ScopedPointer 或 std::unique_ptr。尽可能将对象作为引用而不是指针传递。如果可能,请将其设为 const 引用。
显然要避免使用静态变量和全局变量。有时没有其他选择,但如果有其他选择,那么就使用它,无论需要付出多少努力。
如果分配本地 POD 结构(例如本机代码中的操作系统结构),并且需要用零初始化它,请使用语法
= {};
作为执行此操作的首选。如果由于某种原因不合适,请使用 zerostruct() 函数,或者如果该函数不合适,请使用 zeromem()。避免使用 memset()。将 Component::deleteAllChildren() 作为最后的手段——如果有免费的替代方案,就不要使用它。
juce::ScopedPointer 类是为与 C++11 之前的编译器兼容而编写的,因此尽管它确实为受支持的编译器提供了 C++11 移动功能,但它并不像 std::unique_ptr 那样通用。因此,如果您可以在自己的代码中使用 std::unique_ptr,这可能是更好的选择。我们最终可能会将 JUCE 代码库迁移到 std::unique_ptr。
当从函数返回堆对象时,大多数 JUCE 代码库都早于现代 C++ 风格,即返回 std::unique_ptr 来表明调用者拥有所有权。我们将在某个时候转向这种风格,但与此同时,函数会被注释以明确所有权是如何传递的。
将字符串参数传递给函数
这是个特别的主题,因为这是一个常见问题,也是人们经常做的事情,而且解释起来有点麻烦。在当前形式中,juce::String 是引用计数的,因此按值传递它们很便宜。但是,它不如按引用传递它们便宜。当您想要将字符串传递给函数时,您可以通过多种方式进行:
在大多数情况下,选择哪种方法其实并不重要,因为大多数情况下性能影响并不重要。但如果你想选择最佳方法,规则大致如下:
如果函数要将字符串存储在某个地方,例如将其分配给另一个字符串,或者您打算在函数内部修改它,则按值传递它。如果这样做,您可能还希望将 std::move 移出参数,而不是从参数中复制。
如果函数要存储字符串,并且性能确实至关重要,那么最佳方法是为 const String& 或 String&& 提供两个版本的函数。不过,只有在非常极端的情况下,您才需要付出这么多努力!
如果函数仅读取字符串,并且不需要 String 提供的所有方法,则最好将其作为 StringRef 传递。这样就可以传递字符串文字,而根本不需要创建 String 对象的开销。
如果函数只是读取字符串,但您需要比 StringRef 提供的更多的 String 方法,那么只需将其作为 const String& 传递即可。
课程
首先声明类的公共部分,并将其构造函数和析构函数放在首位。接下来是受保护的项目,然后是私有项目。项目的顺序应大致对应于读者对它们感兴趣的可能性,因此私有实现细节放在最后。
对于简单、简短的内联类,尤其是 cpp 文件中仅在本地用于有限目的的内联类,它们应该使用
struct
而不是,class
因为你通常会拥有所有公共成员,这将节省一行代码来执行初始public:
继承类的布局是将它们放在类名的右侧,垂直对齐。我们在类名前留一个空格,在类名后冒号前留 2-3 个空格,例如
始终为每个继承的类包含 public/private/protected 关键字
将类的成员变量(当然,几乎总是私有的)放在所有公共和受保护的方法声明之后。
任何私有方法都应放在类的末尾,成员变量之后。
如果您的类没有按值复制语义,您可能需要使用 JUCE_DECLARE_NON_COPYABLE 宏。这应该是类声明中的最后一项。或者,
Foo (const Foo&) = delete;
如果效果更好,您可以将语法与其他构造函数一起使用。如果你的类有可能被泄露,那么你可以使用 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR 或 JUCE_LEAK_DETECTOR 宏。
接受单个参数的构造函数通常应标记为
explicit
。显然,有些情况下您确实需要隐式转换,但在编写非显式构造函数之前,请务必仔细考虑。不要使用 NULL、null 或 0 作为空指针!(看在上帝的份上,不要尝试使用 0L,这是最糟糕的!)始终使用 nullptr!
所有 C++“大师”书籍和文章都充满了关于何时最好使用继承与组合的出色而详细的建议。如果您还不熟悉这些问题的公认智慧,那么请阅读一些内容!
各种各样的
else
永远不要在 后面 放 语句return
! LLVM 编码标准 对此有很好的解释,但一旦你仔细想想,就会发现这是基本常识。自从我第一次注意到这一点以来,当我看到其他人的代码中也有这种做法时,它就成了我最讨厌的事情之一!
JUCE 的早期版本使用 T() 宏来包装字符串文字,但该宏已被弃用多年。只需将您的字符串写为普通的旧式 C++ 字符串文字,JUCE String 和 StringRef 类就会处理它们。
对于扩展的 Unicode 字符,将它们嵌入到代码中的唯一完全交叉编译器方法是将其作为转义字符序列编写的 UTF-8 编码 C++ 字符串(这意味着源文件仍然是纯 ASCII,因此文本编辑器无法破坏编码)。如果这样做,您应该将文字包装在 CharPointer_UTF8 类中,这样当转换为字符串时,所使用的格式就很好,而且很清晰。Projucer 有一个内置工具,可以将 Unicode 字符串转换为有效的 C++ 代码并为您处理所有这些。
不要使用宏!当然,在很多情况下,宏是完成任务的正确(或唯一)工具,但请将其视为最后的手段。当然,永远不要使用宏来保存常量值或执行任何可以作为真正的内联函数完成的功能。不用说,您应该给它们起一个不会与其他代码冲突的名字。如果可能的话,在使用它们之后,请用 #undef 取消它们。
使用
++
或--
运算符时,如果可以使用预增量,则切勿使用后增量。虽然对于原始类型来说这并不重要,但预增量是一种很好的做法,因为对于更复杂的对象来说,这样做效率更高。特别是,如果您正在编写 for 循环,请始终使用预增量,例如for (int = 0; i < 10; ++i)
当获取可能为空的指针并仅当其非空时使用它时,请尽可能限制指针的范围 - 例如,不要这样做:
..相反,总是喜欢这样写,这样可以减少指针的范围,从而不可能编写意外使用空指针的代码:
(这也使得代码更清晰,更紧凑)。
当将小型 POD 对象传递到函数中时,您应该始终按值传递它们,而不是按引用传递它们。大多数经验丰富的 C++ 程序员(包括我自己)都习惯于始终将函数参数作为 const 引用传递,例如
const Foo&
。这通常对于复杂对象(例如数组、字符串等)是正确的做法,但是当您传递引用时,它会阻止编译器在调用站点上使用大量优化技术。例如,这意味着编译器通常无法真正知道函数是否会修改原始值(通过 const_cast)或是否会修改与对象位置偏移的内存地址。因此,编写优化器的人给出的最佳实践建议是:尽可能始终坚持按值传递,并且仅在调用复制构造函数的代价非常高时才使用引用。对于总体大小实际上并不比指针大小大很多的小对象,尤其如此。一些应该始终通过值传递的 juce 类包括:Point、Time、RelativeTime、Colour、所有 CharPointer_XYZ 类、Identifier、ModifierKeys、JustificationType、Range、PixelRGB、PixelARGB、Rectangle。我们始终偏爱具有旧 C 等效项的函数的 std 库版本。因此,我们从不使用 fabs、sqrtf、powf 等,而是始终使用多态函数 std::abs、std::sqrt、std::sin、std::cos、std::pow 等。
大多数 juce 容器类使用
int
作为其索引类型,这与标准库对 的使用不同size_t
,但多年来,C++ 标准委员会似乎逐渐认为使用无符号类型是一个错误。然而,这种不匹配确实使得在与 STL 互操作时必须转换索引值有点烦人。(但由于现在许多循环迭代都发生在基于范围的 for 中,所以情况不再那么糟糕了)。我们从不
unsigned
单独使用形容词 -unsigned int
如果您要表达的是这个意思,请始终写出来。形容词单独使用时看起来就像一个未完成的句子。JUCE 一直定义一组基本类型 int8、uint8、int16、uint16、int32、uint32、int64、uint64 - 这些是我们建议在需要特定位大小时使用的。由于标准库引入了 std::uint32_t 等,我们有时也会使用它们,但 juce 的略短一些,从未引起名称冲突的问题。
始终优先使用基于范围的 for 循环来迭代容器,而不是原始的 for 循环!使用 STL 算法和迭代器很棒,但我们尽量不要严格使用它们,因为在许多简单情况下,更直接的方法最终会更具可读性且更易于调试。
我们确实喜欢“几乎总是自动”的风格,但有些情况下最好避免:
最后更新于