Qt高级编码约定

本文翻译自: https://wiki.qt.io/Coding_Conventions
原作者: Qt
原文发布时间:2015年1月14日

  这是我们在编写Qt代码时使用的高级编码约定的概述。有关Qt代码规范,请参见Qt代码风格一文。对于QML,请参阅QML代码规范一文。

C++特性

  • 不要使用异常。
  • 不要使用rtti(运行时类型信息:即typeinfo结构,dynamic_cast或typeid运算符,包括引发异常)。
  • 谨慎明智地使用模板,不仅仅是因为可以使用。
    提示:使用编译自动测试可以查看测试中的所有编译器是否支持C++功能。

Qt源代码中的约定

  • 所有代码仅是ascii(仅7位字符,如果不确定,请运行man ascii).
    • 因为我们内部的语言环境太多,而且UTF-8和latin1系统的组合不健康。通常,您甚至不知道通过单击您喜欢的编辑器中的"保存"就可以破坏字符超过127个字符的范围。
    • 对于字符串:使用\nnn(其中nnn是要在其中输入字符串的任何字符编码的八进制表示形式)或\xnn(其中nn是十六进制)。示例:QString s = QString::fromUtf8("13\005");
    • 对于文档中的变音符号或其他非ASCII字符,请使用qdoc的命令或使用相关的宏。例如\uuml表示ü
  • 每个QObject子类都必须具有Q_OBJECT宏,即使它没有信号或槽也是如此,否则qobject_cast将失败。
  • 在connect语句中规范化信号/槽的参数(请参阅QMetaObject::normalizedSignature),以更快地进行信号/槽查找。您可以使用qtrepotools/util/normalize规范化现有代码。

头文件包含

  • 在公共头文件中,请始终使用以下形式包括Qt头: #include 。库前缀对于Mac OS X框架是必需的,对于非qmake项目也非常方便。
  • 在源文件中,首先包括Qt的头文件,然后是通用的头文件。用空行分隔类别。例:
#include  /* Qt类头文件 */
#include        /* STL 头文件 */
#include  /* 系统头文件 */
  • 如果需要包括qplatformdefs.h请始终将其作为第一个头文件包含
  • 如果您需要包含私有头文件,请当心。不管whatever_p.h位于哪个模块或目录中,请使用以下语法:
#include 

类型转换

  • 避免使用C强制转换,而建议使用C ++强制转换(static_cast,const_cast,reinterpret_cast)。
    • 因为reinterpret_cast和C风格强制转换都是危险的,但是至少reinterpret_cast不会删除const修饰符。
  • 不要使用dynamic_cast,不要对QObject使用qobject_cast或重构设计,例如,通过引入type()方法(请参阅QListWidgetItem)。
  • 使用构造函数强制转换简单类型。例:int(myFloat)代替(int)myFloat
    • 另外重构代码时,编译器会立即通知您是否强制转换会很危险。

编译器/平台的特定问题

  • 使用问号运算符时要格外小心。如果返回的类型不同,则某些编译器会生成在运行时崩溃的代码(您甚至不会收到编译器警告)。例如:
QString s;
return condition ? s : "nothing"; 
// 运行时崩溃:QString与const char *
  • 要非常小心对齐:
    • 每当强制转换指针以增加目标的所需对齐方式时,在某些体系结构上,生成的代码可能会在运行时崩溃。例如,如果将const char *强制转换为const int *,它将在必须将整数对齐为两字节或四字节边界的计算机上崩溃。
    • 使用联合体强制编译器正确对齐变量。在下面的示例中,可以确保AlignHelper的所有实例在整数边界处对齐。
union AlignHelper {
    char c;
    int i;
};
  • 任何具有构造函数或需要运行代码进行初始化的对象都不能用作库代码中的全局对象,因为在运行该构造函数/代码时(在首次使用时,在库加载时,在main()之前或之后,它都是未定义的)。即使为共享库定义了初始化程序的执行时间,在插件中移动该代码或静态编译库时也会遇到麻烦:
/* 全局作用域 */
static const QString x; /* 错误: 需要运行默认构造函数来初始化x。 */
static const QString y = "Hello"; /* 错误: 必须运行接受const char *的构造函数。 */
QString z; /* 超级错误行为! */
static const int i = foo(); /* 错误: foo()调用未定义,可能根本不会被调用。 */

你应该这样做:

/* 全局对象 */
static const char x[] = "someText"; /* 正常工作: 没有构造函数必须运行,x赋值在编译期。*/
static int y = 7; /* 正常工作: y将在编译期设置。*/
static MyStruct s = {1, 2, 3}; /* 正常工作: 编译期静态初始化。*/
static QString *ptr = 0; /* 指向对象的指针是ok的, 不需要运行代码来初始化ptr。*/

使用Q_GLOBAL_STATIC代替创建全局对象:

Q_GLOBAL_STATIC(QString, s)

void foo()
{
    s()->append("moo");
}

注意:作用域中的静态对象没有问题,在第一次使用时,构造函数将会运行。自C++ 11开始,这样的代码是可重入的。

  • 明确定义变量的初始值,不能缺省。
char c; /* c不可能是负的,如果它是无符号的。*/
if (c > 0) { … } /* 不恰当的: c字符一致时无符号字符, 导致条件一直成立。*/
  • 避免64位enum值。
    • 嵌入式ABI接口中所有enum值为32位整型。
    • Microsoft编译器不支持64位enum值。(使用Microsoft®C/C++优化编译器版本15.00.30729.01进行x64的验证)

代码美感

  • 宁可使用enum来定义常量,也不要使用静态const int或define。
    • enum值将在编译时被编译器替换,生成更快的代码。
    • 而使用define不是安全的操作(而且看起来很难看)。
  • 建议参数名字需要完整表达。
    • 大多数IDE将在它们的补全框中显示参数名。
    • 因为它在文档中看起来也更好。
    • 坏代码:doSomething(QRegion rgn, QPoint p)应使用doSomething(QRegion clientRegion, QPoint gravitySource)代替。
  • 当重新实现一个虚方法时,不要再在头文件中放入virtual关键词。在Qt5中,在函数声明;{之前使用override关键词修饰它们。

避免的操作

  • 不要继承模板/工具类
    • 由于析构函数不是virtual,这会导致潜在的内存泄漏问题。
    • 这些符号没有被导出(大部分是内联的),会导致报符号冲突的编译错误提示。
    • 例如:

A库:

class Q_EXPORT X: public QList {};

B库:

class Q_EXPORT Y: public QList {};

导致后果,QList在两个库中导出会报符号冲突的问题。

  • 不要混合使用const和非const迭代器。这将在崩溃的编译器上悄无声息地崩溃。
for (Container::const_iterator it = c.begin(); it != c.end(); ++it) /* 错误的 */
for (Container::const_iterator it = c.cbegin(); it != c.cend(); ++it) /* 正确 */
  • Q[Core]Application是单例类。一次只能有一个实例。但是,该实例可以被销毁,并且可以创建一个新实例,这很可能在ActiveQt或浏览器插件中进行。这样的代码很容易出错:
static QObject *obj = 0;
if (!obj)
    obj = new QObject(QCoreApplication::instance());

需要注意的是:如果QCoreApplication应用程序被销毁,则obj将是悬空指针。对静态全局对象使用Q_GLOBAL_STATIC或对qAddPostRoutine进行清理。

  • 如果可能,请避免使用支持关键字的匿名名称空间。确保使用static本地化到编译单元的名称具有内部链接。不幸的是,对于在匿名名称空间中声明的名称,C++标准要求进行外部链接。

二进制和代码兼容性

  • 定义:
    • Qt 4.0.0是主要版本,Qt 4.1.0是次要版本,Qt 4.1.1是补丁程序版本。
    • 向后二进制兼容性:链接到库的早期版本的代码保持正常工作。
    • 向前的二进制兼容性:链接到新版本库的代码可与旧库一起使用。
    • 源代码兼容性:代码无需修改即可编译。
  • 在次要版本中保持向后二进制兼容性+向后源代码兼容性。
  • 在修补程序版本中保持向前和向后二进制兼容性+向后和向后源代码兼容性:
    • 不要添加/删除任何公共API(例如:全局函数,公共/受​​保护/私有方法)。
    • 不要重新实现方法(甚至不是内联方法,也不是受保护/私有方法)。
    • 检查二进制兼容性解决方案,可以了解b/c的方法。
  • 关于二进制兼容性的信息:https://community.kde.org/Policies/Binary_Compatibility_Issues_With_C++
  • 编写QWidget子类时,请始终重新实现event(),即使它为空。这确保widget可以在不破坏二进制兼容性的情况下得到修复。
  • 从Qt导出的所有函数必须以'q'或'Q'开头。可以使用"symbols"自动测试来验证。

命名空间

阅读命名空间中的Qt[https://wiki.qt.io/Qt_In_Namespace],并记住除测试和Webkit之外的所有Qt都是"namespaced"代码。

操作符

成员与非成员之间的决定[https://stackoverflow.com/questions/4421706/what-are-the-basic-rules-and-idioms-for-operator-overloading/4421729#4421729]

一个对两个参数都一视同仁的二元操作符不应该是成员。因为,除了上述链接提到的原因外。当运算符是成员时,参数也不相等。

QLineF的示例,可惜的是它的operator ==作为成员:

QLineF lineF;
QLine lineN;

if (lineF == lineN) /* 正确:lineN隐式转换为QLineF。 
if (lineN == lineF) /* 错误:QLineF无法隐式转换为QLine,并且LHS是成员,因此不进行转换。*/

如果operator ==在类之外,则转换规则将同样适用于双方。总结:范围小的值不能在前operator==使用

公共头文件的约定

我们的公共头文件必须在某些用户的严格设置下仍然有效。所有已安装的头文件都必须遵循以下规则:

  • 不适用C样式转换(-Wold-style-cast):
    • 使用static_cast,const_cast或reinterpret_cast。
    • 对于基本类型,请使用构造函数形式:int(a)代替(int)a。
    • 有关更多信息,请参见类型转换这一章节。
  • 没有浮点数比较(-Wfloat-equal):
    • 使用qFuzzyCompare将值与增量进行比较。
    • 使用qIsNull来检查浮点数是否为二进制0,而不是将其与0.0进行比较。
  • 不要在子类中隐藏virtual方法(-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  /* 错误的 */
#if defined(Foo) && (Foo == 0) /* 正确的 */
#if Foo - 0 == 0 /* 自认为这种方法很聪明,是吗?请还是老老实实改用上面的正确方法,以提高可读。*/

C++11使用约定

注意:本节尚未被统一接受。本节将作为进一步讨论的基准。

Lambdas

您可以使用具有以下限制的lambda:

  • 如果您使用lambda所在类中的静态函数,请重构代码,以免使用lambda。例如:
void Foo::something()
{
    ...
    std::generate(begin, end, []() { return Foo::someStaticFunction(); });
    ...
}

你应该使用简单的传递函数指针代替:

void Foo::something()
{
    ...
    std::generate(begin, end, &Foo::someStaticFunction);
    ...
}

为什么会出现这一规定(不能在lambda中使用类中的静态函数)?
因为是GCC 4.7和更早版本存在一个错误,需要捕获此错误,但如果您这样做,则Clang 5.0和更高版本将产生警告:

void Foo::something()
{
    ...
    std::generate(begin, end, [this]() { return Foo::someStaticFunction(); });
    /* 警告:不使用lambda捕获'this'[-Wunused-lambda-capture] */
    ...
}

根据以下规则格式化lambda:

  • 即使函数不带参数,也要始终在参数列表中写括号。
[]() { doSomething(); }

不要这样写:

[] { doSomething(); }
  • 在第一行上放置捕获列表,参数列表,返回类型和左括号,在下一行缩进主体,在新行上将右括号括起来。
[]() -> bool {
    something();
    return isSomethingElse();
}

不要这样写:

[]() -> bool { something();
    somethingElse(); }
  • 将封闭函数调用的右括号和分号与lambda的右括号放在同一行:
foo([]() {
    something();
});
  • 如果在'if'语句中使用lambda,请在新行上写lambda,以避免在lambda的左括号和'if'语句的左括号之间造成混淆:
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会使代码的可读性降低,请不要使用auto。请记住,代码的看的次数比编写的次数要多。

  • 避免在同一条语句中重复某个类型。
auto something = new MyCustomType;
auto keyEvent = static_cast(event);
auto myList = QStringList() << QLatin1String("FooThing") << QLatin1String("BarThing");
  • 分配迭代器类型时使用auto。
auto it = myList.const_iterator();

更多精彩内容请关注公众号Qt君

你可能感兴趣的:(Qt高级编码约定)