本文翻译自 Coding Conventions
目录
本文是对 Qt 上层代码规范的概述。对于底层代码规范,参见 Qt Coding Style。对于 QML 的代码规范,参见 QML Coding Conventions。
C++ 特性
- 不要使用异常
- 不要使用 rtti(Run-Time Type Information,运行时类型信息;即,
type_info
结构、dynamic_cast
或typeid
运算符以及抛出异常) - 恰当地使用模板而非滥用。
提示:可以使用 compile
自动测试(autotest)来查看某一 C++ 特性是否被测试场中的所有编译器支持。
Qt 源码规范
- 所有代码只采用 ASCII 字符(即仅 7 bit 字符,若不确定,在 Linux 下可运行
run ascii
查看所有 ASCII 字符)- 理由:本地语言环境复杂,且 UTF-8 编码和 latin1 编码混杂。通常,在编辑器里保存后,编码值大于 127 的字符编码会被自动拆分。
- 对于字符串:使用
\nnn
(其中nnn
是任意字符串编码的八进制表示),或使用\xnn
(其中nn
为十六进制)。例如:QString s = QString::fromUtf8("13\005");
- 对于文档中的变音符号或其他非 ASCII 字符,使用 qdoc 的命令或使用相关的宏来表示;e.g. 使用
\uuml
表示 ü。
- 即使没有使用信号和槽,每个 QObject 的子类也应包含
Q_OBJECT
宏,否则qobject_cast
可能会出错。 - 在 connect 语句中规范化信号和槽的参数(参见
QMetaObject::normalizedSignature
),从而加快信号和槽的查找速度。可以使用qtrepotools/util/normalize
对现有代码进行规范化。
头文件包含
-
在公共头文件中,包含 Qt 的头文件格式为:
#include <QtCore/qwhatever.h>
。库前缀对于 Mac OS X 框架是必要的,同时也方便了非 qmake 项目。 -
在源文件中,应先包含专用的头文件,再包含通用的头文件,并以空行分隔。
#include <qstring.h> // Qt class #include <new> // STL stuff #include <limits.h> // system stuff
-
若要包含头文件
qplatformdefs.h
,应将其放在所有头文件的最前面。 -
包含私有头文件时要谨慎。无论
whatever_p.h
位于哪个模块或目录下,均使用以下语法:#include <private/whatever_p.h>
强制类型转换
- 避免使用 C 风格的强制类型转换,多使用 C++ 风格(
static_cast
,const_cast
,reinterpret_cast
)- 理由:尽管
reinterpret_cast
和 C 风格强制类型转换都有危险性,但至少reinterpret_cast
不会除去const
修饰符。
- 理由:尽管
- 对 QObject 对象不要使用
dynamic_cast
,而应使用qobject_cast
,或者重构,例如引入type()
方法(参见 QListWidgetItem) - 使用构造函数来进行简单的类型转换:使用
int(myFloat)
而非(int)myFloat
- 理由:当重构代码时,该方法使得编译器可以立即告知类型转换是否危险。
编译器/平台具体问题
-
使用条件运算符时要格外谨慎。如果返回类型不一致,某些编译器生成的代码会直接在运行时崩溃(编译器甚至不会发出警告)。
QString s; return condition ? s : "nothing"; // crash at runtime - QString vs. const char *
-
要格外注意字节对齐。
- 若指针被强制类型转换,导致目标要求的对齐字节数增加时,生成的代码可能在某些框架结构下崩溃。例如,如果一个
const char *
被转换成const int *
,其在要求整型量必须 2 字节或 4 字节对齐的机器上会崩溃。 - 使用联合体来强制使编译器正确地对齐变量。以下示例可以保证所有
AlignHelper
的实例都在整型边界处对齐。
union AlignHelper { char c; int i; };
- 若指针被强制类型转换,导致目标要求的对齐字节数增加时,生成的代码可能在某些框架结构下崩溃。例如,如果一个
-
任何使用构造函数或需要运行代码来初始化的对象都不能作为全局对象,因为其构造函数/初始化代码在何时运行(首次使用时、加载库时、进入 main() 之前或不运行)是未定义的。即使共享库的初始化的运行时间是已定义的,当把代码移植到插件,或者库采用静态编译时,也会出现问题。
// global scope static const QString x; // Wrong - default constructor needs to be run to initialize x static const QString y = "Hello"; // Wrong - constructor that takes a const char * has to be run QString z; // super wrong static const int i = foo(); // wrong - call time of foo() undefined, might not be called at all
可以使用以下形式创建全局对象:
// global scope static const char x[] = "someText"; // Works - no constructor must be run, x set at compile time static int y = 7; // Works - y will be set at compile time static MyStruct s = {1, 2, 3}; // Works - will be initialized statically, no code being run static QString *ptr = 0; // Pointers to objects are ok - no code needed to be run to initialize ptr
作为代替,使用
Q_GLOBAL_STATIC
来创建全局对象:Q_GLOBAL_STATIC(QString, s) void foo() { s()->append("moo"); }
注意:在作用域中使用 static 对象是没问题的,当首次进入该作用域时,其构造函数会被调用。从 C++ 11 开始,这样的代码是可重入的。
-
在不同的架构体系中,一个
char
类型的变量可能是带符号或者无符号的。如果明确需要带符号或无符号的字符型变量,应使用singed char
或unsinged char
。对于默认字符型为无符号的平台,下列代码中的条件始终为真:char c; // c can't be negative if it is unsigned /********/ /*******/ if (c > 0) { … } // WRONG - condition is always true on platforms where the default is unsigned
-
避免使用 64 位的枚举值
- AAPCS 内嵌的 ABI 把所有枚举值硬编码为 32 位整型数。
- Microsoft 的编译器不支持 64 位的枚举值(已在 Microsoft ® C/C++ Optimizing Compiler Version 15.00.30729.01 for x64 上测试)。
代码美观性
-
相比使用
static const int
或宏定义,更倾向于使用枚举来定义常量。- 枚举值将在编译期被编译器替换,从而使代码运行更快。
- 宏定义不是命名空间安全的(并且看起来很丑)。
-
倾向于在头文件中使用较长(完整)的参数名。
- 大多数 IDE 会在补全框中显示参数名。(PS:自动补全大法好)
- 较长的参数名在文档中更具可读性。
- 不好的风格:
doSomething(QRegion rgn, QPoint p)
——使用doSomething(QRegion clientRegion, QPoint gravitySource)
来代替。
-
当重新实现虚方法时,不要在头文件里使用
virtual
关键字。在 Qt5 中,应在函数声明之后,即在
;
或{
之前,添加override
关键字作为注解。
应避免的情况
-
不从模板/工具类继承
-
析构函数时非虚的,将导致潜在的内存泄露。
-
符号是未导出的(并且是内联的),将导致符号冲突。
-
举例:库 A 中有
class Q_EXPORT X: public QList<QVariant> {};
库 B 中有
class Q_EXPORT Y: public QList<QVariant> {};
QList<QVariant>
的符号从两个库中导出——冲突。
-
-
不要混淆 const 和非 const 迭代器,这会导致在编译器上崩溃:
for (Container::const_iterator it = c.begin(); it != c.end(); ++it) // WRONG for (Container::const_iterator it = c.cbegin(); it != c.cend(); ++it) // Right
-
Q[Core]Application 是单例类,在任意时刻只能有一个实例。但是,可以销毁该实例再重新构造一个新的实例。这种情况很可能出现在 ActiveQt 或浏览器插件中。类似下列的代码很容易崩溃:
static QObject *obj = 0; if (!obj) obj = new QObject(QCoreApplication::instance());
如果
QCoreApplication
应用被销毁,obj
将成为悬挂指针。使用Q_GLOBAL_STATIC
或使用qAddPostRoutine
进行清理。 -
避免使用匿名空间,尽量使用
static
关键字。编译单元中使用static
关键字的名称会保证拥有内部连接性。但对于在匿名空间中声明的名称,C++ 标准强制其为外部链接。(7.1.1/6, or see various discussions about this on the gcc mailing lists)
二进制和源代码兼容性
-
给出以下定义:
- Qt 4.0.0 代表一个主要版本,Qt 4.1.0 代表一个次要版本,Qt 4.1.1 代表一个补丁版本
- 二进制向后兼容:链接到库的早期版本的代码在新版本库上仍然有效
- 二进制向前兼容:链接到新版本库的代码与在旧版本库上也有效
- 源代码兼容:代码无需修改即可正常编译
-
在次要版本之间保证二进制向后兼容和源代码向后兼容
-
在补丁版本之间保证二进制向后、向前兼容和源代码向后、向前兼容
- 不添加/删除任何公有 API(e.g. 全局函数、共有/保护/私有方法)
- 不重新实现方法(包括内联、保护/私有方法)
- 关于保证二进制兼容的方法,参见 Binary Compatibility Workarounds
-
关于二进制兼容的信息:
https://community.kde.org/Policies/Binary_Compatibility_Issues_With_C++
-
若创建一个 QWidget 的子类,则应始终重新实现
event()
,即使其为空。这样可以确保修复 widget 时不会破坏二进制兼容。 -
Qt 导出的所有函数都必须以“q”或“Q”开头。使用“symbols”自动测试(autotest)来找出不合规范的函数。
命名空间
参见 Qt In Namespace,记住除了 Tests 和 Webkit 之外的所有 Qt 代码都是包含在命名空间内的代码。
运算符
“The decision between member and non-member”
一个参数地位平等(PS:参数互换位置后返回值不变)的二元运算符不应是成员函数。因为,除了以上 Stack Overflow 回答中提到的原因外,当运算符是成员函数时,参数是不平等的。
下例中 QLineF
将运算符 ==
重载为成员函数:
QLineF lineF;
QLine lineN;
if (lineF == lineN) // Ok, lineN is implicitly converted to QLineF
if (lineN == lineF) // Error: QLineF cannot be converted implicitly to QLine, and the LHS is a member so no conversion applies
若运算符 ==
在类之外,则转换规则对两边均相同。
公共头文件规范
我们的公共头文件必须满足使用者设置的严格要求。所有安装的头文件必须遵循以下规则:
-
避免 C 风格的强制类型转换(
-Wold-style-cast
)-
使用
static_cast
,const_cast
或reinterpret_cast
-
对于基本类型,使用构造函数的形式:如使用
int(a)
而非(int)a
-
更多信息参见强制类型转换章节
-
-
不要直接比较浮点数(
-Wfloat-equal
)- 使用
qFuzzyCompare
来进行考虑浮点精度误差的模糊比较 - 使用
qIsNull
来检验浮点数是否为二进制 0,而不要直接与 0.0 比较
- 使用
-
不要在子类中隐藏虚函数(
-Woverloaded-virtual
)-
如果一个基类
A
有虚函数virtual int val()
且子类B
将其重载为int val(int x)
,则A
的val
函数会被隐藏。使用using
关键字使其可见:class B: public A { using A::val; int val(int x); };
-
-
不要覆盖变量(
-Wshadow
)- 避免类似于
this->x = x;
的语句 - 变量名不要与类中声明的函数同名
- 避免类似于
-
在检测预定义变量的值之前,一定要先检验其是否已定义(
-Wundef
)#if Foo == 0 // WRONG #if defined(Foo) && (Foo == 0) // Right #if Foo - 0 == 0 // Clever, are we? Use the one above instead, for better readability
C++ 11 使用规范
注意:本节尚未成为规范,但其内容会是日后进一步讨论的基础。
lambda 表达式
可以在以下限制条件下使用 lambda 表达式:
-
若在含有 lambda 的类中使用了静态函数,则应重构代码使其不含 lambda 表达式。例如,对于以下代码
void Foo::something() { ... std::generate(begin, end, []() { return Foo::someStaticFunction(); }); ... }
作为代替,可以简单地直接传递函数指针:
void Foo::something() { ... std::generate(begin, end, &Foo::someStaticFunction); ... }
这是因为 GCC 4.7 及其之前的版本存在 bug,需要捕获
this
指针,但是如果这样做,Clang 5.0 及其之后的版本又会产生一个警告:void Foo::something() { ... std::generate(begin, end, [this]() { return Foo::someStaticFunction(); }); // warning: lambda capture 'this' is not used [-Wunused-lambda-capture] ... }
按以下规则排版 lambda 表达式:
-
始终保留参数列表的小括号,即便函数并不接受任何参数:
[]() { doSomething(); }
而不应省略小括号:
[] { doSomething(); }
-
将捕获列表、参数列表、返回类型和左大括号放在第一行,函数体换行并缩进,右大括号另起一行:
[]() -> bool { something(); return isSomethingElse(); }
而非
[]() -> bool { something(); somethingElse(); }
-
外围函数的调用运算符中的右括号以及分号应与 lambda 表达式的右大括号在同一行:
foo([]() { something(); });
-
如果 if 条件判断语句中有 lambda 表达式,则 lambda表达式应另起一行,以防止混淆二者的左大括号:
if (anyOf(fooList, [](Foo foo) { return foo.isGreat(); })) { return; }
而非
if (anyOf(fooList, [](Foo foo) { return foo.isGreat(); })) { return; }
-
如果合适,也可以将 lambda 表达式放在一行:
foo([]() { return true; }); if (foo([]() { return true; })) { ... }
auto 关键字
可以在以下情况中使用 auto
关键字。但是如果有疑问,例如使用 auto
导致代码可读性降低,则不应使用。记住代码是一次写成,多次阅读的。
-
若可以避免在同一条语句中类型重复出现:
foo([]() { return true; }); if (foo([]() { return true; })) { ... }
-
当为迭代器赋值时:
auto it = myList.const_iterator();