Qt Creator 编码规则

注:本文件正在不断完善。

编码规则的目的是引导 Qt Creator 开发者,帮助他们编写易于理解和维护的代码,并尽量减少困惑和意外。

通常,规则并非一成不变。如果您有充分的理由要打破一条规则,那么可以这样做。但首先请确保至少有其他一些开发者同意您的做法。

为了对 Qt Creator 的主要源代码做出贡献,您应遵守以下规则

  • 最重要的规则是:KISS(保持简单)。始终选择更简单而不是更复杂的实现方案,这会使维护变得更容易。
  • 编写好的 C++ 代码。也就是说,可读性强,必要时经过良好注释,并面向对象。
  • 利用 Qt。不要重新发明轮子。考虑一下您的代码哪些部分足够通用,可以将其整合到 Qt 而不是 Qt Creator。
  • 将代码适应到 Qt Creator 中现有的结构。如果您有改进的想法,在编写代码之前应与其他开发者讨论。
  • 遵循《代码结构》、《格式化》和《模式和最佳实践》中的指南。
  • 记录接口。目前我们使用 qdoc,但正在考虑将其改为 doxygen。

提交代码

要将代码提交到 Qt Creator,您必须了解工具和机制,以及 Qt 开发的哲学。有关如何设置 Qt Creator 的工作开发环境以及如何提交代码和文档以供包含的更多信息,请参见 Qt 贡献指南

二进制和源代码兼容性

以下列表描述了发布版本如何编号,并定义了发布之间的二进制兼容性源代码兼容性

  • Qt Creator 3.0.0 是一个 主要版本,Qt Creator 3.1.0 是一个 次要版本,而 Qt Creator 3.1.3 是一个 修复版本
  • 向后二进制兼容性意味着与库的早期版本链接的代码仍能正常工作。
  • 向前二进制兼容性意味着与较新版本库链接的代码也能与旧版本库一起工作。
  • 源代码兼容性意味着代码无需修改即可编译。

我们目前在主要版本和次要版本之间不能保证二进制兼容性或源代码兼容性。

但是,我们尽量保护同一个小版本内的补丁版本的后向二进制兼容性和后向源代码兼容性。

  • 软 API 冻结:在一个小版本-beta 发布后不久,我们开始保持该小版本及其补丁版本内的后向源代码兼容性。这意味着从那时起,使用 Qt Creator API 的代码将编译对此小版本的 API 的所有后续版本(包括补丁版本)。仍可能存在一些规则外的例外,应进行适当沟通。
  • 严格的API冻结:从次版本的候选版本开始,我们保持该版本及其修补版本的向下源代码兼容性。我们也开始保持向下二进制兼容性,但请注意,这不会反映在插件兼容性设置中。因此,针对候选版本的Qt Creator插件在实际版本或更高版本中实际上无法加载,即使理论上库的二进制兼容性允许这样做。下面是有关插件规范的说明。
  • 严格的ABI冻结:从次版本的最终版本开始,我们保持本版本及其所有修补版本向下源代码和二进制兼容性。

为了保持向下兼容

  • 不要添加或移除任何公共API(例如全局函数、公共/受保护/私有成员函数)。
  • 不要重新实现函数(包括内联函数,以及受保护或私有函数)。
  • 检查二进制兼容性解决方案,以了解如何保留二进制兼容性。

有关二进制兼容性的更多信息,请参阅C++的二进制兼容性问题

插件元数据的角度来看,这意味着

  • 修补版本中的Qt Creator插件将具有次版本的compatVersion。例如,来自3.1.2版本的插件将具有compatVersion="3.1.0"
  • 次版本的预发布版,包括候选版本,仍然将自身作为兼容版本,这意味着编译针对最终版本的插件将无法加载到预发布版中。
  • Qt Creator插件开发者可以在声明依赖其他插件时设置相应的version来决定他们的插件是否需要特定修补版本(或更高版本)的Qt Creator插件,或者是否与当前版本的所有修补版本兼容。Qt项目提供的Qt Creator插件的默认做法是需要最新的修补版本。

例如,Qt Creator 3.1 beta(内部版本号3.0.82)中的Find插件将具有

<plugin name="Find" version="3.0.82" compatVersion="3.0.82">
  <dependencyList>
    <dependency name="Core" version="3.0.82"/>
    ....

Qt Creator 3.1.0最终版本的Find插件将具有

<plugin name="Find" version="3.1.0" compatVersion="3.1.0">
  <dependencyList>
    <dependency name="Core" version="3.1.0"/>
    ....

Qt Creator 3.1.1修补版本的Find插件将具有3.1.1版本,与Find插件3.1.0版本(compatVersion="3.1.0")向后二进制兼容,并需要一个与Core插件二进制向后兼容的Core插件版本3.1.1

<plugin name="Find" version="3.1.1" compatVersion="3.1.0">
  <dependencyList>
    <dependency name="Core" version="3.1.1"/>
    ....

代码结构

遵循代码结构的指南以使代码更快、更清晰。此外,这些指南还允许您利用C++的强类型检查。

  • 尽量使用前增量而非后增量。前增量有可能比后增量快。只需思考一下前/后增量的明显实现即可。此规则也适用于减量
    ++T;
    --U;
    
    -NOT-
    
    T++;
    U--;
  • 尽量减少对同一代码的重复评估,特别是对于循环
    Container::iterator end = large.end();
    for (Container::iterator it = large.begin(); it != end; ++it) {
            ...;
    }
    
    -NOT-
    
    for (Container::iterator it = large.begin();
         it != large.end(); ++it) {
            ...;
    }

格式化

标识符的大小写

在标识符中使用驼峰式命名法

按以下方式对标识符中的第一个单词进行大写

  • 类名以大写字母开头。
  • 函数名以小写字母开头。
  • 变量名以小写字母开头。
  • 枚举名和值以大写字母开头。未限定的枚举值包含枚举类型的部分名称。

空白

  • 使用四个空格进行缩进,不使用制表符。
  • 在适合的地方使用空白行来分组语句。
  • 始终只使用一个空白行。

指针和引用

对于指针或引用,在星号 (*) 或和号 (&) 前总是使用一个空格,但之后则不要用。尽可能避免使用 C 风格的强转。

char *blockOfMemory = (char *)malloc(data.size());
char *blockOfMemory = reinterpret_cast<char *>(malloc(data.size()));

-NOT-

char* blockOfMemory = (char* ) malloc(data.size());

当然,在这种情况下,使用 new 可能是一个更好的选择。

运算符名称和括号

不要在运算符名称和括号之间使用空格。等号 (==) 是运算符名称的一部分,因此空格会使得声明看起来像是一个表达式。

operator==(type)

-NOT-

operator == (type)

函数名称和括号

不要在函数名称和括号之间使用空格。

void mangle()

-NOT-

void mangle ()

关键字

关键字后面和花括号前面总是使用单个空格。

if (foo) {
}

-NOT-

if(foo){
}

注释

一般来说,在 "//" 后面使用一个空格。为了在多行注释中对齐文本,可以插入多个空格。

花括号

作为基本规则,左花括号应该放在语句行的开始。

if (codec) {
}

-NOT-

if (codec)
{
}

异常:函数实现和类声明总是将左花括号放在一行开始位置。

static void foo(int g)
{
    qDebug("foo: %i", g);
}

class Moo
{
};

当条件语句的体包含多行,或者单行语句相当复杂时,请使用花括号。否则省略。

if (address.isEmpty())
    return false;

for (int i = 0; i < 10; ++i)
    qDebug("%i", i);

-NOT-

if (address.isEmpty()) {
    return false;
}

for (int i = 0; i < 10; ++i) {
    qDebug("%i", i);
}

异常 1:如果父语句跨越多行或是一行内的引申,请也使用花括号。

if (address.isEmpty()
        || !isValid()
        || !codec) {
    return false;
}

注意:

if (address.isEmpty())
    return false;

if (!isValid())
    return false;

if (!codec)
    return false;

异常 2:如果 if-then-else 块中的 if-code 或 else-code 涵盖多行,请也在这些块中使用花括号。

if (address.isEmpty()) {
    --it;
} else {
    qDebug("%s", qPrintable(address));
    ++it;
}

-NOT-

if (address.isEmpty())
    --it;
else {
    qDebug("%s", qPrintable(address));
    ++it;
}
if (a) {
    if (b)
        ...
    else
        ...
}

-NOT-

if (a)
    if (b)
        ...
    else
        ...

当条件语句的体为空时,请使用花括号。

while (a) {}

-NOT-

while (a);

括号

使用括号来组合表达式。

if ((a && b) || c)

-NOT-

if (a && b || c)
(a + b) & c

-NOT-

a + b & c

换行

  • 保持行长度不超过100个字符。
  • 如有必要,请插入换行。
  • 逗号放在换行句尾。
  • 运算符从新行的开始处开始。
    if (longExpression
        || otherLongExpression
        || otherOtherLongExpression) {
    }
    
    -NOT-
    
    if (longExpression ||
        otherLongExpression ||
        otherOtherLongExpression) {
    }

声明

  • 对于您类中的访问部分,使用此顺序:public、protected、private。public 部分对类的每个用户都很有趣。private 部分仅对类实现者(您)感兴趣。
  • 避免在类声明文件中声明全局对象。如果相同的变量对所有对象都使用,请使用静态成员。
  • 使用 class 而不是 struct。一些编译器会将这种区别混合到符号名称中,如果 struct 声明之后跟着类定义,它们会发出警告。为了避免在这些之间不断地进行更改,我们声明 class 为首选方式。

声明变量

  • 避免全局类的变量,以排除初始化顺序问题。如果无法避免,考虑使用 Q_GLOBAL_STATIC
  • 声明全局字符串字面量如下
    const char aString[] = "Hello";
  • 尽可能避免使用短名称(如,a、rbarr、nughdeget)。仅当变量目的明显时,才使用单字符变量名称。
  • 每行声明一个变量。
    QString a = "Joe";
    QString b = "Foo";
    
    -NOT-
    
    QString a = "Joe", b = "Foo";

    注意:QString a = "Joe" 正式调用了一个从字符串字面量构造的临时对象的复制构造函数。因此,它可能比直接通过 QString a("Joe") 构造更昂贵。然而,编译器允许省略复制(即使这可能产生副作用),并且现代编译器通常这么做。考虑到这些成本相等,Qt Creator代码更喜欢 '=' 习惯用法,因为它与传统C样式的初始化一致,不会被误认为是函数声明,并且减少更多初始化中嵌套括号的级别。

  • 避免缩写
    int height;
    int width;
    char *nameOfThis;
    char *nameOfThat;
    
    -NOT-
    
    int a, b;
    char *c, *d;
  • 等到需要时再声明变量。这一点在初始化同时进行时尤为重要。

命名空间

  • 将左大括号放在与 namespace 关键字同一行。
  • 不要在内部缩进声明或定义。
  • 可选但推荐:如果命名空间跨越多行,则在右侧大括号后添加一个注释,重复命名空间。
    namespace MyPlugin {
    
    void someFunction() { ... }
    
        }  // namespace MyPlugin
  • 作为例外,如果命名空间内只有一个类声明,则所有内容可以放在同一行
    namespace MyPlugin { class MyClass; }
  • 不要在头文件中使用 using 指令。
  • 定义类和函数时不要依赖于 using 指令,而是在正确命名的声明区域中定义。
  • 访问全局函数时不要依赖于 using 指令。
  • 在其他情况下,鼓励使用 using 指令,因为它们可以帮助您避免代码混乱。建议将所有 using 指令放在文件顶部,在所有包含之后。
    [in foo.cpp]
    ...
    #include "foos.h"
    ...
    #include <utils/filename.h>
    ...
    using namespace Utils;
    
    namespace Foo::Internal {
    
    void SomeThing::bar()
    {
        FilePath f;              // or Utils::FilePath f
        ...
    }
    ...
    } // Foo::Internal           // or // namespace Foo::Internal
    
    -NOT-
    
    [in foo.h]
    ...
    using namespace Utils;       // Wrong: no using-directives in headers
    
    class SomeThing
    {
        ...
    };
    
    -NOT-
    
    [in foo.cpp]
    ...
    using namespace Utils;
    
    #include "bar.h"             // Wrong: #include after using-directive
    
    -NOT-
    
    [in foo.cpp]
    ...
    using namepace Foo;
    
    void SomeThing::bar()        // Wrong if Something is in namespace Foo
    {
        ...
    }

模式和惯例

命名空间

阅读 Qt In Namespace,并记住 Qt Creator 中的所有代码都是 命名空间感知 代码。

Qt Creator 中的命名空间策略如下

  • 导出给其他库或插件使用的库或插件的类/符号位于特定的库/插件命名空间中,例如 MyPlugin
  • 未导出的库或插件的类/符号位于额外的 Internal 命名空间中,例如 MyPlugin::Internal

传递文件名

Qt Creator API 预期文件名以可移植格式,即在 Windows 上也使用斜杠 (/) 而不是反斜杠 (\)。要从用户传递文件名到 API,首先使用 QDir::fromNativeSeparators 进行转换。要将文件名呈现给用户,使用 QDir::toNativeSeparators 将其转回到本地格式。对于这些任务,考虑使用 Utils::FilePath::fromUserInput(QString) 和 Utils::FilePath::toUserOutput().

比较文件名时使用 Utils::FilePath,因为这将考虑大小写敏感性。还请确保比较干净的路径(QDir::cleanPath())。

要使用的类和不要使用的类

Qt Creator 代码的很大一部分处理与开发机器不同的设备的上的数据。这些可能在路径分隔符、行结束、进程启动详情等方面有所不同。

然而,一些基本的 Qt 类假定 Qt 应用程序只关注与开发机器类似的机器。

因此,这些类不适用于关心非本地代码的 Qt Creator 部分。相反,Qt Creator 的 Utils 库提供了替代品,导致以下规则:

插件依赖

为了使 Qt Creator 可扩展,我们致力于最大限度地减少插件和外部库之间的硬运行时依赖。

这里有一些实现此目的的技术。

通过回调扩展基础插件功能

此模式允许叶插件通过注入回调为中央插件提供额外的功能。

这种模式适用于在某些设置中有用但在其他情况下不适用或被认为侵入性强的功能(例如,因为大小或外部依赖),因此用户可能希望启用或禁用它,而不会影响他们其余的设置。

叶插件可以在此基础上依赖大型的(或不适用的)外部依赖,而不会将此依赖加到中央插件上。

整体模式如下

  • centralplugin/somewhere.h
    std::function<void(...)> &fancyLeafCallback();
    
    void possiblyFancyOperation();
  • centralplugin/somewhere.cpp
    std::function<void(...)> &fancyLeafCallback()
    {
        static std::function<void(...)> callback;
        return callback;
    }
    ...
    
    void possiblyFancyOperation()
    {
       if (auto callback = fancyLeafCallback()) {
          // can be used
          callback();
       } else {
          // not present, use some plain fallback implementation
          // or error out
       }
    }
  • leafplugin/leafplugin.cpp
    void LeafPlugininitialize()
    {
       ...
       Central::fancyLeafCallback() = [this](...) { doSomething; };
       ...
    }

插件扩展点

插件扩展点是一个由一个插件提供并由其他插件实现的接口。插件将检索该接口的所有实现并使用它们。也就是说,它们扩展了插件的功能。通常,接口的实现会在插件初始化期间放入全局对象池中,并在插件初始化结束时检索。

例如,Find 插件提供了 FindFilter 接口供其他插件实现。使用 FindFilter 接口可以添加额外的搜索范围,这些范围将显示在 高级搜索 对话框中。Find 插件从全局对象池中检索所有 FindFilter 实现,并在对话框中显示它们。插件将实际搜索请求转发给正确的 FindFilter 实现,然后执行搜索。

使用全局对象池

您可以通过 ExtensionSystem::PluginManager::addObject() 将对象添加到全局对象池中,并通过 ExtensionSystem::PluginManager::getObject() 再次检索特定类型的对象。这主要用于 插件扩展点 的实现。

注意:不要将单例放入池中,也不要从池中检索它。而是使用单例模式。

C++特性

  • 优先使用 #pragma once 而不是头部守卫。
  • 除非您知道自己在做什么,否则请勿使用异常。
  • 请勿使用RTTI(运行时类型信息;即typeinfo结构,dynamic_cast或typeid运算符及其抛出异常),除非你明确知道自己在做什么。
  • 除非你明确知道自己在做什么,否则请勿使用虚拟继承。
  • 请注意合理使用模板,而不仅仅是能够使用。

    提示:使用编译器自测来查看测试农场中的所有编译器是否支持某个C++特性。

  • 所有代码仅支持ASCII字符(7位字符,如果不确定,请运行man ascii
    • 理由:我们内部有太多的locale设置,UTF-8和Latin1系统混合使用也不健康。通常,字符>127可以由你点击保存在你的编辑器中而被破坏,即使你并未察觉。
    • 对于字符串:使用\nnn(其中nnn是你想要的字符串的locale的八进制表示)或\xnn(其中nn是十六进制)。例如: QString s = QString::fromUtf8("\213\005");
    • 对于文档中的重音符号或其他非ASCII字符,要么使用qdoc \unicode 命令,要么使用相关的宏。例如:\uuml 用于ü。
  • 尽可能使用static关键词替代匿名命名空间。带有static的名称在本编译单元中将保证具有内部链接。对于在匿名命名空间中声明的名称,C++标准不幸地强制要求外部链接(ISO/IEC 14882, 7.1.1/6,或者您可以在gcc邮件列表中看到关于这个的多个讨论)。

空指针

对于空指针常量,请使用nullptr。

void *p = nullptr;

-NOT-

void *p = NULL;

-NOT-

void *p = '\0';

-NOT-

void *p = 42 - 7 * 6;

注意:作为一种例外,导入的第三方代码以及涉及本机API(src/support/os_*)的代码可以使用NULL或0。

lambda表达式

按以下规则格式化lambda表达式

  • 当lambda既不接收参数也不指定返回类型时,省略圆括号。
    [] { ... lambda body ... }
    
    -NOT-
    
    []() { ... lambda body ... }
  • 在定义lambda时,将方括号与圆括号粘合。
    [](int a) { ... lambda body ... }
    
    -NOT-
    
    [] (int a) { ... lambda body ... }
  • 将捕获列表、参数列表、返回类型和开括号放在第一行,然后在下面的行中缩进身体,并在新的一行中放置闭括号。
    []() -> bool {
        something();
        return isSomethingElse();
    }
    
    -NOT-
    
    []() -> bool { something();
    somethingElse(); }
  • 将包含函数调用的闭括号和分号与其lambda的闭括号在同一行。
    foo([] {
        something();
    });
  • 如果你在一个if语句中使用lambda表达式,将lambda从新的一行开始,以避免混淆lambda的开括号和if语句的开括号。
    if (anyOf(fooList,
            [](Foo foo) {
                return foo.isGreat();
            }) {
        return;
    }
    
    -NOT-
    
    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<QKeyEvent *>(event);
    auto myList = QStringList({ "FooThing",  "BarThing" });
  • 当分配迭代器类型时。
    auto it = myList.const_iterator();

局部化枚举

在需要避免未局部化枚举的隐式转换为int或额外的范围有用的地方,您可以使用局部化枚举。

委托构造函数

如果多个构造函数实质上使用相同的代码,则请使用委托构造函数。

初始化列表

使用初始化列表来初始化容器,例如

const QList<int> values = {1, 2, 3, 4, 5};

使用大括号进行初始化

如果您使用大括号进行初始化,请遵循与圆括号相同的规则。例如

class Values // the following code is quite useful for test fixtures
{
    float floatValue = 4; // prefer that for simple types
    QList<int> values = {1, 2, 3, 4, integerValue}; // prefer that syntax for initializer lists
    SomeValues someValues{"One", 2, 3.4}; // not an initializer_list
    SomeValues &someValuesReference = someValues;
    ComplexType complexType{values, otherValues} // constructor call
}

object.setEntry({"SectionA", value, doubleValue}); // calls a constructor
object.setEntry({}); // calls default constructor

但是请注意不要过度使用它,以免混淆代码。

非静态数据成员初始化

在公共导出类之外,使用非静态数据成员初始化进行简单的初始化。

默认删除函数

考虑使用 =default=delete 来控制特殊函数。

重写

在重写虚函数时,建议使用 override 关键字。不要在重写的函数上使用 virtual。

确保一个类一致地使用 override,要么对所有重写的函数使用,要么一个都不使用。

基于范围的 for 循环

您可以使用基于范围的 for 循环,但请注意虚假解引用的问题。如果 for 循环只读取容器并且不清楚容器是否是 const 或非共享的,请使用 std::cref() 来确保容器不会被不必要地解引用。

std::optional

避免使用抛出异常的函数 value()。首先检查值的可用性,然后使用非抛出异常的函数来访问值,例如 operator*operator->。在非常简单的情况下,您也可以使用 value_or()

if (optionalThing) {
    val = optionalThing->member;
    other = doSomething(*optionalThing);
}

-NOT-

if (optionalThing) {
    val = optionalThing.value().member;
    other = doSomething(optionalThing.value());
}

使用 QObject

  • 请记住将 Q_OBJECT 宏添加到依赖元对象系统的 QObject 子类。与元对象系统相关功能包括信号和槽的定义、使用 qobject_cast<> 以及其他功能。另见 Casting
  • 优先使用 Qt5 风格的 connect() 调用。
  • 当使用 Qt4 风格的 connect() 调用时,对 connect 语句内的信号和槽的参数进行归一化以安全地使信号和槽查找变得更快。您可以使用 $QTDIR/util/normalize 来归一化现有代码。更多信息,请参阅 QMetaObject::normalizedSignature
  • 避免使用 QObject::sender()。请显式传递发送者。
    connect(action, &QAction::triggered, this, [this, action] {
        onActionTriggered(action);
    });
    ...
    void MyClass::onActionTriggered(QAction *action)
    {
        ... use action ...
    }
    
    -NOT-
    
    connect(action, &QAction::triggered, this, &MyClass::onActionTriggered);
    ...
    void MyClass::onActionTriggered()
    {
        QAction *action = qobject_cast<QAction *>(sender());
        if (action == nullptr)
            return;
    
        ... use action ...
    }
  • 避免使用 QSignalMapper。请使用 lambda 并通过 lambda 捕获传递映射的值。

文件头

如果您创建了一个新文件,则文件的开头应包含一个与 Qt Creator 中其他源文件中相同的头注释。

包含头文件

  • 使用以下格式来包含 Qt 头文件:#include <QWhatEver>。不要包含模块,因为它可能在 Qt4 和 Qt5 之间更改。
  • 按照从具体到一般的顺序排列包含项,以确保头文件是自含的。例如
    • #include "myclass.h"
    • #include "otherclassinplugin.h"
    • #include <otherplugin/someclass.h>
    • #include <QtClass>
    • #include <stdthing>
    • #include <system.h>
  • 将其他插件的头文件用尖括号 (<>) 而不是引号 ("") 包围起来,以便更容易在源代码中找到外部依赖。
  • 在长块的 peer 头部之间添加空行,并尽量在一个块中将头文件按字母顺序排列。

强制类型转换

  • 避免使用C风格强制类型转换,优先使用C++风格的强制类型转换(static_castconst_castreinterpret_cast)。reinterpret_cast和C风格的强制类型转换都存在风险,但至少reinterpret_cast不会移除const修饰符。
  • 不要使用dynamic_cast,对于QObject使用qobject_cast或重构设计,例如通过引入一个type()函数(参见QListWidgetItem),除非你知道自己在做什么。

编译器和平台特定问题

  • 使用问号运算符时请格外小心。如果返回的类型不相同,一些编译器可能会生成在运行时崩溃的代码(你甚至不会接收到编译器警告)
    QString s;
    // crash at runtime - QString vs. const char *
    return condition ? s : "nothing";
  • 关于对齐要格外小心。

    每次当一个指针进行强制类型转换,使得目标所需的对齐级别增加时,在某些架构上生成的代码可能在运行时崩溃。例如,如果将const char *强制类型转换为const int *,在那些整数需要对齐在两个字节或四个字节边界的机器上将会崩溃。

    使用联合来强制编译器正确对齐变量。在下面的例子中,你可以确保所有AlignHelper实例都得到了正确的整数边界对齐。

    union AlignHelper
    {
        char c;
        int i;
    };
  • 对于头文件中的静态声明,始终坚持使用整型类型、整型数组的数组以及它们的结构。例如,static float i[SIZE_CONSTANT];在大多数情况下不会得到优化并复制在每一个插件中,最好避免这样做。
  • 任何具有构造函数或需要运行代码以进行初始化的东西都不能用作库代码中的全局对象,因为当构造函数或代码将运行时(首次使用时,在库加载时,在main()之前或根本不运行时)是不确定的。

    即使对于共享库,初始化器的执行时间已经定义,当你将代码移动到插件中或者库被静态编译时,你将会遇到麻烦。

    // global scope
    
    -NOT-
    
    // Default constructor needs to be run to initialize x:
    static const QString x;
    
    -NOT-
    
    // Constructor that takes a const char * has to be run:
    static const QString y = "Hello";
    
    -NOT-
    
    QString z;
    
    -NOT-
    
    // Call time of foo() undefined, might not be called at all:
    static const int i = foo();

    你可以做的事情

    // global scope
    // No constructor must be run, x set at compile time:
    static const char x[] = "someText";
    
    // y will be set at compile time:
    static int y = 7;
    
    // Will be initialized statically, no code being run.
    static MyStruct s = {1, 2, 3};
    
    // Pointers to objects are OK, no code needed to be run to
    // initialize ptr:
    static QString *ptr = 0;
    
    // Use Q_GLOBAL_STATIC to create static global objects instead:
    
    Q_GLOBAL_STATIC(QString, s)
    
    void foo()
    {
        s()->append("moo");
    }

    注意:函数作用域中的静态对象没有问题。构造函数将在函数第一次进入时运行。但代码不是可重入的。

  • char是有符号的还是无符号的取决于架构。如果你明确想使用有符号或无符号的char,请使用有符号的charuchar。以下代码在PowerPC上可能会损坏。
    // Condition is always true on platforms where the
    // default is unsigned:
    if (c >= 0) {
        ...
    }
  • 避免使用64位的枚举值。AAPCS(ARM架构的进程调用标准)嵌入式ABI将所有枚举值硬编码为32位整数。
  • 不要混合const和无const迭代器。这会导致在有问题的编译器上无声地崩溃。
    for (Container::const_iterator it = c.constBegin(); it != c.constEnd(); ++it)
    
    -NOT-
    
    for (Container::const_iterator it = c.begin(); it != c.end(); ++it)
  • 不要在导出类中内联虚拟析构函数。这会导致依赖插件的vtable重复,并可能破坏RTTI。参见QTBUG-45582

美学

  • 优先使用无作用域枚举来定义const,而不是使用静态const int或宏定义。枚举值将在编译时由编译器替换,从而生成更快的代码。而宏定义不是命名空间安全的。
  • 在头文件中优先使用详细的参数名称。Qt Creator将在其完成框中显示参数名称。这将使文档看起来更好。

从模板或工具类继承

从模板或工具类继承有以下几个潜在的问题

  • 析构函数不是虚拟的,这可能导致内存泄漏。
  • 符号不是导出的(并且大部分是内联的),这可能导致符号冲突。

例如,库A有一个名为Q_EXPORT X: public QList<QVariant> {};的类,库B有一个名为Q_EXPORT Y: public QList<QVariant> {};的类。突然,QList符号从两个库中导出,导致冲突。

继承与聚合对比

  • 如果有明确的“是”关系,请使用继承。
  • 使用聚合来重用正交的构建块。
  • 如果有选择,优先使用聚合而不是继承。

公共头文件规范

我们的公共头文件必须能经受住一些用户严格的设置。所有安装的头文件都必须遵循以下规则

  • 不要使用C样式的转换(-Wold-style-cast)。对于基本类型,使用构造函数形式:int(a)代替(int)a。更多信息,请参阅转换
  • 不要进行浮点数比较(-Wfloat-equal)。使用qFuzzyCompare来比较具有偏差的值。使用qIsNull来检查浮点数是否为二进制0,而不是将其与0.0比较,或者首选,将此类代码移入实现文件。
  • 不要在子类中隐藏虚函数({-Woverloaded-virtual})。如果基类A有一个虚函数int val(),而子类B有一个同名重载,则A中的val函数会被隐藏。使用using关键字使其再次可见,并添加以下愚蠢的工作方式以解决破坏编译器的编译
    class B: public A
    {
    #ifdef Q_NO_USING_KEYWORD
    inline int val() { return A::val(); }
    #else
    using A::val;
    #endif
    };
  • 不要隐藏变量(-Wshadow)。
  • 如果可能,避免使用如this->x = x;这样的操作。
  • 不要将变量的名称与类中声明的函数相同。
  • 为了提高代码可读性,始终在探测预处理器变量的值之前检查它是否已定义(-Wundef)。
    #if defined(Foo) && Foo == 0
    
      -NOT-
    
    #if Foo == 0
    
    -NOT-
    
    #if Foo - 0 == 0
  • 当使用defined运算符检查预处理器定义时,始终将变量名放在圆括号内。
    #if defined(Foo)
    
      -NOT-
    
      #if defined Foo

类成员名称

我们使用“m_”前缀约定,除了公共结构体成员(通常在*私有类中)和非常罕见的真正公共结构的情况)。dq指针不受“m_”规则的约束。

指向d指针的(“Pimpls”)命名为“d”,而不是“m_d”。在class Foo中,d指针的类型是FooPrivate *,其中FooPrivate在同一个命名空间中声明为Foo,或者在Foo导出时,在相应的{Internal}命名空间中。

如果需要(例如,当私有对象需要发出适当的类的信号时),FooPrivate可以是Foo的朋友。

如果私有类需要一个对真实类的反向引用,指针被命名为q,其类型是Foo *。(与Qt中的约定相同:“q”看起来像是“d”的倒置。)

不要使用智能指针来保护d指针,因为它会产生编译和链接时间开销,并导致包含更多符号的更重的对象代码,从而导致例如减慢调试器启动速度。

############### bar.h

#include <QScopedPointer>
//#include <memory>

struct BarPrivate;

struct Bar
{
    Bar();
    ~Bar();
    int value() const;

    QScopedPointer<BarPrivate> d;
    //std::unique_ptr<BarPrivate> d;
};

############### bar.cpp

#include "bar.h"

struct BarPrivate { BarPrivate() : i(23) {} int i; };

Bar::Bar() : d(new BarPrivate) {}

Bar::~Bar() {}

int Bar::value() const { return d->i; }

############### baruser.cpp

#include "bar.h"

int barUser() { Bar b; return b.value(); }

############### baz.h

struct BazPrivate;

struct Baz
{
    Baz();
    ~Baz();
    int value() const;

    BazPrivate *d;
};

############### baz.cpp

#include "baz.h"

struct BazPrivate { BazPrivate() : i(23) {} int i; };

Baz::Baz() : d(new BazPrivate) {}

Baz::~Baz() { delete d; }

int Baz::value() const { return d->i; }

############### bazuser.cpp

#include "baz.h"

int bazUser() { Baz b; return b.value(); }

############### main.cpp

int barUser();
int bazUser();

int main() { return barUser() + bazUser(); }

结果

Object file size:

 14428 bar.o
  4744 baz.o

  8508 baruser.o
  2952 bazuser.o

Symbols in bar.o:

    00000000 W _ZN3Foo10BarPrivateC1Ev
    00000036 T _ZN3Foo3BarC1Ev
    00000000 T _ZN3Foo3BarC2Ev
    00000080 T _ZN3Foo3BarD1Ev
    0000006c T _ZN3Foo3BarD2Ev
    00000000 W _ZN14QScopedPointerIN3Foo10BarPrivateENS_21QScopedPointerDeleterIS2_EEEC1EPS2_
    00000000 W _ZN14QScopedPointerIN3Foo10BarPrivateENS_21QScopedPointerDeleterIS2_EEED1Ev
    00000000 W _ZN21QScopedPointerDeleterIN3Foo10BarPrivateEE7cleanupEPS2_
    00000000 W _ZN7qt_noopEv
             U _ZN9qt_assertEPKcS1_i
    00000094 T _ZNK3Foo3Bar5valueEv
    00000000 W _ZNK14QScopedPointerIN3Foo10BarPrivateENS_21QScopedPointerDeleterIS2_EEEptEv
             U _ZdlPv
             U _Znwj
             U __gxx_personality_v0

Symbols in baz.o:

    00000000 W _ZN3Foo10BazPrivateC1Ev
    0000002c T _ZN3Foo3BazC1Ev
    00000000 T _ZN3Foo3BazC2Ev
    0000006e T _ZN3Foo3BazD1Ev
    00000058 T _ZN3Foo3BazD2Ev
    00000084 T _ZNK3Foo3Baz5valueEv
             U _ZdlPv
             U _Znwj
             U __gxx_personality_v0

文档

文档是从源文件和头文件生成的。您为其他开发人员编写文档,而不是为自己。在头文件中,指定接口。也就是说,函数做什么,而不是实现。

在.cpp文件中,如果实现不是显而易见的,您可以记录实现。

©2024 The Qt Company Ltd. 本文件中包含的文档贡献是各自所有者的版权。此处提供的文档是根据自由软件基金会发布的《GNU自由文档许可证》(版本1.3)授权使用的,请访问https://gnu.ac.cn/licenses/fdl.html获取详细信息。Qt及其相关标志是 Finnish Qt Company Ltd 在芬兰以及其他国家/地区的商标。所有其他商标均为其各自所有者的财产。