当前位置: 首页 > news >正文

CppCon 2017 学习:Effective Qt: 2017 Edition

这段内容讲的是 Qt 容器(Qt Containers)和标准库容器(STL Containers)之间的选择和背景:

主要观点:

  1. Qt 容器的历史背景
    • Qt 自身带有一套容器类(如 QList, QVector, QMap 等),主要是因为历史原因:
      • 早期Qt需要支持没有标准库(STL)支持的平台。
      • 避免将标准库的符号暴露在Qt的ABI(应用二进制接口)中,保证二进制兼容性。
  2. 现在的情况(Qt 5及以后)
    • Qt 5以后,已经假定目标平台有“可用的、可用的STL实现”。
    • 这意味着Qt本身内部开始依赖标准库,标准库的可用性已成为前提。
  3. Qt容器在API中的角色
    • Qt依然在API中使用Qt容器,且这些容器对应用程序开发者开放。
    • 但对于新项目和业务代码,建议优先使用标准库容器,只有在必要时才用Qt容器。

总结

  • Qt容器是为了历史兼容和API稳定性存在。
  • 对于大多数现代C++项目,尤其是跨平台和与第三方库交互,推荐使用标准库容器(如 std::vector, std::map 等)
  • 仅在和Qt框架接口交互时,或者特殊性能需求时考虑Qt容器。

这部分内容详细对比了 Qt 容器和 C++ 标准库容器的设计哲学及对应关系,并介绍了一个特殊的 Qt 容器——QVarLengthArray。以下是总结:

Qt 容器设计哲学 vs 标准库设计哲学

特点Qt容器标准库容器
目标足够好,主要用于构建 GUI真正通用,适合各种场景
API 风格使用 camelCase(例如 isEmpty()使用 snake_case(例如 empty()
设计重点易用性和API易发现性高效和正确性
典型用法示例QVector<int> v; v << 1 << 2 << 3;std::vector<int> v; v.push_back(1);
拷贝行为拷贝可能较“便宜”(浅拷贝或引用计数机制)拷贝通常是深拷贝
算法实现作为成员函数(如 contains()通过标准算法(如 std::find

Qt与标准库对应容器对照表

Qt 容器标准库容器
QVectorstd::vector
QList
QLinkedListstd::list
QVarLengthArray
QMapstd::map
QMultiMapstd::multimap
QHashstd::unordered_map
QMultiHashstd::unordered_multimap
QSetstd::unordered_set

QVarLengthArray 介绍

  • QVarLengthArray 是 Qt 中一个特殊的容器,类似于 std::vector,但它预先在栈上分配一定的空间,避免频繁的堆分配。
  • 类似于 Boost 的 small_vector,支持所谓的“短小优化”(SSO,small string optimization思想),提高小规模数据的性能。
  • 适用于大多数情况下容器元素数量不会超过某个固定值的场景。
  • 示例声明:QVarLengthArray<Obj, 32> vector; 表示预分配32个对象的空间。

这段内容进一步强调了 QList 以及整体 Qt容器 的局限性,并建议更倾向使用标准库容器。总结如下:

QList 的特性和问题

  • 不是链表,而是基于数组实现的列表。
  • 对于存储大于指针大小的对象非常低效,因为它会为每个对象单独分配堆内存。
  • 建议避免使用 QList,除非别无选择。
  • 自己写代码时优先使用 QVector

不建议使用 Qt 容器的理由

  • Qt 容器维护和更新不活跃,缺乏新特性。
  • STL 容器更快,生成的代码更小且经过更多测试。
  • Qt 容器功能远不及 STL,例如:
    • 存放的类型必须可默认构造和可复制。
    • 没有异常安全保证。
    • 缺少许多 C++98/C++11/C++17 新增的API,如范围构造、插入、就地构造(emplace)、基于节点的操作等。
    • 不支持灵活的分配器、比较器、哈希函数等自定义操作。
  • Qt 容器的API不一致,比如 QVector<T> 支持 append(T&&),但 QList<T> 不支持。
  • 还有在 resize、capacity、shrink 等行为上的差异。
  • Qt 容器API 与 STL 容器存在微妙差异,可能带来使用上的困扰。

建议

  • 优先使用 STL 容器(如 std::vectorstd::mapstd::unordered_map 等)。
  • 只有在必须与 Qt API 交互时,才考虑使用 Qt 容器。

这段主要给出了在实际项目中选择容器的建议,核心点总结如下:

选择哪个容器?

  • STL 容器大多数情况下性能和特性都优于 Qt 容器。
  • Qt 自身实现内部也开始采用 STL 容器,说明它们的优势。
  • Qt 的 API 仍然暴露 Qt 容器,无法轻易替换,因为 Qt 有强 API 和 ABI 兼容性承诺

应用程序的推荐策略:

  1. 优先使用 STL 容器。
  2. 仅在以下情况考虑使用 Qt 容器:
    • 没有对应的 STL 或 Boost 容器(这几乎不存在)。
    • 与 Qt 或基于 Qt 的库接口交互时。
  3. 如果用到了 Qt 容器,尽量避免来回转换 STL 和 Qt 容器。
    保持使用 Qt 容器,减少性能开销和复杂度。
    简单来说,除非为了兼容 Qt 接口,推荐用 STL 容器,既现代又高效。

关于 resize、capacity、shrink 这几个行为,Qt 容器和 STL 容器确实存在一些细节差异:

1. resize()

  • STL 容器
    • resize(n) 会调整容器大小到 n,如果变大,会用默认值或指定值填充新增元素。
    • 对于 std::vector,新增元素构造且初始化。
    • 可以保证元素连续且大小准确。
  • Qt 容器(比如 QVector
    • resize(n) 也会调整大小,但内部实现可能采用引用计数共享数据。
    • 新增元素初始化行为和 STL 类似,但某些情况下效率可能略差。
    • QList 的行为因内部结构不同,resize() 可能导致额外的内存分配,效率不佳。

2. capacity()

  • STL 容器
    • capacity() 返回当前已分配但未使用的内存空间大小。
    • std::vector 会预先分配一定空间,减少扩容次数。
    • 可以通过 reserve() 来预先分配容量,避免多次重新分配。
  • Qt 容器
    • capacity() 也返回预分配空间大小。
    • 但 Qt 容器(尤其是老版本)对容量管理不如 STL 灵活,扩容策略可能不同。
    • 不能像 STL 一样明确调用 reserve() 保证容量。

3. shrink_to_fit()

  • STL 容器
    • C++11 引入的函数,shrink_to_fit() 用于请求减少容量以匹配当前大小。
    • 实现是非强制的,但多数现代库会释放多余内存。
    • 提高内存利用率,避免浪费。
  • Qt 容器
    • 大多数 Qt 容器没有 shrink_to_fit() 接口。
    • 只能通过拷贝或交换技巧手动释放多余容量,比如重新构造一个容器拷贝数据。
    • 缺乏直接控制容量的函数,不够灵活。

额外说明

  • Qt 容器内部通常使用引用计数和共享数据的技术,这导致某些操作(比如 resize)会更复杂,可能出现延迟复制(copy-on-write)。
  • STL 容器行为更加透明直接,便于性能优化和行为预测。

总结

操作STL 容器Qt 容器
resize()直接调整大小,初始化新增元素类似,但可能因共享数据延迟复制
capacity()返回预分配容量,可用 reserve 控制返回预分配容量,容量管理不够灵活
shrink_to_fit()标准接口,尝试释放多余内存无对应接口,需手动技巧释放多余容量

总结来说,针对 Qt 6:

  • Qt 容器必须继续保留,确保兼容性和稳定的 API/ABI,不会做大破坏性改动。
  • QList 在 Qt 中使用非常广泛,但它其实并不是一个理想的线性容器。
  • 未来有可能让 QList 直接成为 QVector 的别名(typedef),简化内部实现。
  • 同时,Qt 可能会推出一个新的容器类型(比如 QArrayList)来替代 QList 的部分功能,提供更好的性能和设计。
  • Qt 容器通过类型特征(type traits)来优化性能,尤其是判断一个类型是否支持relocatable(可重定位)
  • 如果类型是可重定位的,容器扩容时可以直接用 realloc 这样高效的内存操作,而不需要一个个移动元素,性能大幅提升。
  • 使用 Qt 容器时,建议用 Q_DECLARE_TYPEINFO 宏来告诉 Qt该类型是否可重定位,从而启用优化。
  • 一些典型例子:
    • 简单结构体(如 IntVector)通常是可重定位的。
    • 有指针指向自己或有内部联系的结构(如 TreeNode)通常不可重定位,因为移动内存会破坏指针。
    • 有短字符串优化(SSO)的字符串类型,如果内部指针指向内部缓冲区,也不可重定位。
      这个机制可以显著提高 Qt 容器的性能,前提是正确声明类型信息。

编译器不能自动判断类型是否可重定位(relocatable),需要开发者手动标注。

  • Qt 通过宏 Q_DECLARE_TYPEINFO(Type, Kind) 来告诉容器该类型的“性质”,Kind 可以是:
    • Q_PRIMITIVE_TYPE
      • 类型非常简单,比如 int,任何位模式都是有效的
      • 构造和析构可以跳过,直接内存拷贝即可
    • Q_MOVABLE_TYPE
      • 类型可被内存移动(如用 memmoverealloc
      • 但仍然调用构造和析构函数
    • Q_COMPLEX_TYPE(默认)
      • 普通复杂类型,需要正常调用构造、复制、析构
  • EASTL 有类似机制(EASTL_DECLARE_TRIVIAL_RELOCATE),而 STL 标准库本身没有明确这个特性。
    这让 Qt 容器能根据类型特性选择最优内存操作,提高性能。

每次定义可能会被放入 Qt 容器的自定义类型时,都应该用 Q_DECLARE_TYPEINFO 显式声明其类型信息。

  • 例如:
    struct IntVector {int size, capacity;int *data;
    };
    Q_DECLARE_TYPEINFO(IntVector, Q_MOVABLE_TYPE);
    
  • 如果之后再加这个 trait,有可能会导致 ABI 兼容性问题,影响程序稳定性和升级安全。
    所以,建议一开始就定义好,避免后期修改带来的麻烦。

Qt 的**隐式共享(Implicit Sharing)**核心思想是:

  • 对象内部包含一个指向实际数据(pimpl)的指针,这个数据块有一个引用计数器(refcount)。
  • 创建对象时,refcount = 1。
  • 拷贝对象时,只拷贝指针,refcount +1。
  • 调用 const 方法不改数据,refcount 不变。
  • 调用非 const 方法时,如果 refcount > 1,说明数据被共享,必须先detach(深拷贝数据),保证修改不会影响其他对象(写时拷贝,Copy-On-Write)。
    这样设计的好处是:
  • 拷贝操作很轻量(只增引用计数),节省性能。
  • 保证数据修改时不会影响到其他对象,实现值语义。
  • 但需要注意调用非 const 方法会触发隐式深拷贝,可能会有性能开销。
    这个机制常见于 Qt 的字符串(QString)、容器等类。
    要小心“隐藏的 detach”,即你可能没有意识到调用了非 const 方法,导致了拷贝开销。

这里演示了隐式共享在 QVector 中的实际效果。

示意过程是这样的:

QVector<int> v {10, 20, 30};
QVector<int> v2 = v;  // 复制v,不会马上拷贝数据,而是共享内部数据
  • vv2 共享同一块内存(payload),里面存着 {10, 20, 30}。
  • 引用计数(refcount)为 2,表示两个 QVector 对象共享同一数据。
  • 这时,内存只保存了一份数据,拷贝成本很低。
    只有当你对 vv2 调用非 const 方法(修改操作)时,如果 refcount > 1,就会触发 detach,深拷贝数据,分配独立内存,避免数据冲突。
    总结:
  • 复制 QVector 很轻量(共享数据 + refcount++)
  • 修改共享数据前会触发深拷贝(detach)

这个例子具体展示了隐式共享(copy-on-write)机制在 QVector 修改时的行为:

QVector<int> v {10, 20, 30};
QVector<int> v2 = v;  // v2 共享 v 的数据,refcount = 2,payload = {10, 20, 30}
v2[0] = 99;  // 修改 v2,第一个元素变成 99

过程分析:

  • 初始时,vv2 共享同一份数据(payload),内容是 {10, 20, 30},引用计数是 2。
  • 当执行 v2[0] = 99 这个写操作时,v2 检测到引用计数大于 1(表示数据被共享),触发detach
  • detach 意味着 v2 会进行一次深拷贝,分配自己的内存来存储数据。
  • 修改只会影响 v2v 依旧保持原数据 {10, 20, 30}
  • 结果是:
    • v 仍然是 {10, 20, 30}
    • v2 变成了 {99, 20, 30}
    • 两者的数据不再共享,引用计数分别为 1。
      这个机制保证了:
  • 复制对象时开销很小,都是共享数据。
  • 只有写操作时才真正做深拷贝,保证数据安全。
    这是 Qt 容器里隐式共享的核心思想,也是性能优化的关键点。

Qt 的 Implicit Sharing(隐式共享) 的总结。以下是对这段内容的详细理解解释:

什么是 Implicit Sharing?

隐式共享是一种 “写时拷贝”(Copy-On-Write, COW) 机制,结合了引用计数和延迟深拷贝的技术,核心目的是:

  • 节省内存
  • 提高性能
  • 简化代码书写

为什么有用?

“This mechanism makes writing code a lot simpler”

这句话的意思是:你可以像写普通值一样写 Qt 的类(例如 QString, QByteArray, QVector 等),而不用担心性能问题。
比如:

QString a = "hello";
QString b = a; // 不会拷贝数据,只是增加引用计数
b[0] = 'H';    // 触发深拷贝(detach),a 仍然是 "hello",b 变成 "Hello"

你可以放心用:

  • 值返回(return QString
  • 拷贝赋值(QString b = a
  • 不需要频繁考虑性能陷阱

哪些 Qt 类支持 Implicit Sharing?

“The great majority of Qt value classes are implicitly shared”

常见支持隐式共享的类包括:

类型是否支持隐式共享
QString
QByteArray
QVariant
QImage / QPixmap
QVector
QList
QMap / QHash
QVarLengthArray

例外:QVarLengthArray 是值语义容器,不走引用计数,它在栈上直接分配数据,所以不隐式共享。

工作原理简述:

  1. 拷贝对象时,不复制数据,只是引用计数 +1。
  2. 写入对象时,如果引用计数 > 1,就自动复制一份(detach),以避免影响其他对象。
  3. 析构对象时,引用计数 -1,当为 0 时释放资源。

总结:

隐式共享是 Qt 的一大特色。它允许你:

  • 写出高效、简洁的值语义代码;
  • 不担心性能陷阱;
  • 但也要注意:写操作(即使看起来是 const)可能隐式触发深拷贝。

Implicit sharing and containers: where’s the catch?”指出了 Qt 隐式共享容器的一些隐藏陷阱和误区。以下是逐句解析与理解:

隐式共享和容器:问题出在哪?

• Handing out references to data inside a container does not make the container unshareable

意思:
即使你将容器中的元素通过引用的方式返回或传递出去,比如:

QVector<int> v1 = {1, 2, 3};
QVector<int> v2 = v1; // 现在引用计数是 2
int &ref = v1[0]; // 获取引用

此时 v1 仍然是可共享的,Qt 不会自动触发 detach。
也就是说:只是访问引用,不会破坏共享关系。

• It's easy to accidentally detach a container

意思:
一旦你对容器做了写操作(哪怕是间接的),就会触发 detach(深拷贝)。比如:

v2[0] = 100; // 一写就会 detach,变成独立的副本

这种操作很容易发生在你没意识到的地方,从而悄悄改变了对象的共享状态。

• Accidental detaching can hide bugs

意思:
这种悄悄发生的 detach 行为可能导致 bug 被隐藏,因为:

  • 你以为两个对象共享同一份数据(如 v1, v2),但其实不再共享;
  • 导致数据不同步、调试困难;
  • 在多线程或资源受限环境中尤其危险。
    例如:
if (v1 == v2) {doSomething(); // 你以为它们是同一份数据,但可能早就 detach 了
}

• IOW, it's not just about performance

IOW = In Other Words(换句话说)
不是只有性能问题,还是“正确性问题”!

  • 深拷贝带来的性能开销固然重要;
  • 代码逻辑混乱共享状态错乱数据不一致更加危险;
  • 这些 bug 可能非常隐蔽,特别是当代码中混入了隐藏的 detach 操作。

• Code polluted by (out-of-line) detach/destructor calls

意思:
编译后的代码里会因为 Qt 的隐式共享机制,出现许多:

  • 隐藏的拷贝构造函数调用
  • 深拷贝(detach)操作
  • 析构函数调用
    这会让代码生成“变重”、函数调用栈变复杂,甚至会破坏 inlining,从而降低性能或调试可读性。

总结:使用 Qt 隐式共享容器的注意事项

项目建议
写入操作明确知道何时触发了 detach
多对象共享容器时小心副作用、不可预期的独立副本
性能敏感代码尽量使用 std::vector 等无隐式共享的 STL 容器
传引用/指针访问内部数据知道不会破坏共享状态,但不要写入!

这段内容解释了 Qt 隐式共享容器(如 QVector)中一个容易被忽略的陷阱引用(包括迭代器)不会阻止容器被拷贝(detach),可能导致代码行为与你预期的不同。

下面是逐句解释和深入理解:

Returning references to data inside a container(从容器中返回引用)

“Handing out references to data inside a container does not make the container unshareable”

意思是:
即使你取出了容器中某个元素的引用,这个容器仍然是“共享的”,Qt 不会因为你持有引用而主动拷贝(detach)数据
也就是说,这不是 COW(Copy-On-Write)触发的条件。

“E.g. of such references: iterators”

像下面这样:

QVector<int> v = {10, 20, 30};
auto it = v.begin();     // 拿到迭代器
int &ref = v[0];         // 或直接取引用

这些引用/迭代器不会改变引用计数,也不会让 Qt 自动 detach。

例子分析:

QVector<int> v {10, 20, 30};
auto &r = v[0];        // 取引用
QVector<int> v2 = v;   // 现在 v 和 v2 是共享的,refcount = 2
r = 99;                // 修改通过 v 的引用,会影响共享数据!

图示如下(内存共享前):

v 和 v2 共用同一个 payload:
payload = [10, 20, 30]
refcount = 2

当执行 r = 99; 时,没有触发 detach,因为 r 是直接引用底层数据。
结果是:

v  = [99, 20, 30]
v2 = [99, 20, 30]    <-- 也被修改了!

你以为 v2[0] 还是 10,结果 断言失败:

assert(v2[0] == 10); //  fails!

关键陷阱总结:

行为是否触发 detach?说明
赋值一个容器不触发引用计数增加
调用非 const 成员函数可能触发v[0] = 99,会自动复制(detach)
手动获取元素引用然后修改不自动 detach数据是共享的,两个容器都会变
使用迭代器修改内容不自动 detach同样直接影响共享内存

正确做法建议:

  1. 避免持久使用引用或迭代器后再修改容器副本
  2. 如果你想“安全修改一个副本”,请手动 detach:
    QVector<int> v2 = v;
    v2.detach();           // 强制深拷贝
    v2[0] = 99;            // 不会影响原 v
    
  3. 使用 STL 容器(如 std::vector)时,这种问题不会出现,因为没有隐式共享。

Qt 隐式共享容器中「意外的深拷贝(accidental detach)」问题。这是 Qt 使用者常常忽略的一大坑。以下是详细解释:

你需要理解的核心要点:

例子:

QVector<int> calculateSomething();
const int firstResult = calculateSomething().first(); 

问题分析:

QVector<T>::first(); // 是非 const 的,返回 T&

这就意味着:

  • calculateSomething() 生成了一个临时的 QVector<int>
  • .first()非 const 成员函数,所以 Qt 会 触发 detach(即 deep copy 临时对象的数据)。
    但其实你并不需要修改这个容器!你只是想拿第一个值!但 Qt 没法知道你的意图,调用了非 const 版本,就要执行 Copy-On-Write。

结果:

你只是想:

const int x = QVector<int>{10, 20, 30}.first(); 

但 Qt 背后悄悄:

  • 创建 QVector 临时对象
  • 进行一次深拷贝(detach)
  • 然后返回引用(其实没用到)
    这就引发了不必要的内存分配和复制——而你完全没有意识到!

正确的写法:

const int firstResult = calculateSomething().constFirst();

这样:

  • .constFirst()const 成员函数
  • 不会触发 detach
  • 没有不必要的深拷贝
  • 返回值仍然是你需要的第一个元素(但是只读的)

总结:Qt 中意外 detach 的教训

错误写法原因正确写法
v.first()非 const 成员函数,可能 deep copyv.constFirst()
v.last()同上v.constLast()
v[i]返回引用,非 const,可能 detachv.at(i)const auto val = v[i];

怎么发现这些问题?

  • 它们在编译时不会报错;
  • 但会在 heap profiler(如 massif, heaptrack)中看到内存突增;
  • 一旦你分析出代码里这些细节,会发现许多“不该拷贝的地方在偷偷拷贝”。

Qt 容器的隐式共享(implicit sharing)机制以及 accidental detach(意外拷贝) 带来的陷阱。现在我们来逐条解析你说的这个更严重的问题:

1. Accidental Detach 导致的 Bug(不是性能问题,而是逻辑错误)

场景代码:

QMap<int, int> map;
// ...
if (map.find(key) == map.cend()) {std::cout << "not found" << std::endl;
} else {std::cout << "found" << std::endl;
}

问题来了:

  • map.find(key) 是一个 非常量成员函数(non-const)
  • 它可能导致容器 detach,即做一次深拷贝(复制 pimpl)。
  • 而你已经提前调用了 map.cend(),它指向 原始容器的末尾
  • 之后容器被 detach 成新副本,map.find() 返回的是 新副本的迭代器
  • 两个 end 迭代器 来自不同的容器副本,它们 不相等!

结果:

哪怕 key 根本就不在原始 map 中,也可能打印:

found

这就不是性能问题了,而是一个 逻辑 Bug,非常隐蔽、危险!

正确做法:使用 const 方法

if (map.constFind(key) == map.cend()) {std::cout << "not found" << std::endl;
}

或者,等价更安全:

if (!map.contains(key)) {std::cout << "not found" << std::endl;
}

总结建议:Qt 容器 + 隐式共享 + 非 const 方法 = 潜在 Bug

场景错误方法原因正确做法
查找元素map.find(key)可能触发 detach,破坏逻辑判断map.constFind(key)
访问第一个元素v.first()非 const 方法可能导致 deep copyv.constFirst()
遍历时比较混用 iteratorconst_iterator来自不同容器副本,比较结果错误统一用 const_iterator

🗣 标准库的立场 vs Qt 的立场

C++ STLQt
避免隐式共享机制(copy-on-write)依赖 implicit sharing
强调明确语义、值语义更偏重方便与 API 一致性
更安全、更一致更方便、更快捷,但埋雷多
如果你开发的是性能敏感或逻辑严谨的模块(比如底层库、工具链),建议:
  • 尽量使用 STL 容器;
  • 只在与 Qt API 交互时使用 Qt 容器;
  • 严格区分 const 和非 const 使用;
  • 使用静态分析工具(如 Clazy)来检测 Qt-specific misuse。

千万不要再用 foreachQ_FOREACH

原因总结:

foreach (var, container)

等价于以下代码:

{const auto _copy = container;  //  拷贝了整个容器!auto it = _copy.begin(), end = _copy.end();for (; it != end; ++it) {var = *it;body;}
}

严重问题:

1. 容器整体拷贝一次

即使你只想遍历,但 Qt 容器采用隐式共享机制(copy-on-write),会把整个容器 复制一份!这完全是你意料之外的。

2. 逻辑错误隐患

容器变了,你拿到的是副本,里面元素可能不对,还以为遍历的是原始容器。

3. 性能问题非常严重

在有大量数据或频繁迭代的场景下,每次循环都在悄悄 deep copy。

正确做法:使用 C++11 的 range-based for

for (const auto& value : container) {// Safe, efficient, no copy
}
  • 不会触发隐式深拷贝
  • 更现代,更清晰
  • 完美支持 STL 和 Qt 容器(如 QVector, QStringList

尤其小心 Qt 容器

由于 Qt 容器隐式共享(implicit sharing)+ Q_FOREACH 的复制行为,一起使用等于 踩雷必炸
例如:

QStringList list = {"a", "b", "c"};
foreach (QString s, list) {// 修改 s 没问题,但 list 是被拷贝的副本
}

你以为你在修改 list,其实根本没改到!

永久禁用建议(企业/团队级别)

.proCMakeLists.txt 中添加:

DEFINES += QT_NO_FOREACH

这会在你代码中使用 foreach / Q_FOREACH编译失败

总结

特性Q_FOREACHC++11 range-for
是否复制容器会复制不会
支持 STL 容器
是否安全高效
是否推荐使用完全不推荐强烈推荐

foreach / Q_FOREACH 使用建议总结

“优点”(其实是个误导):

  • “可以安全修改原容器”
    其实是误导:因为循环中你操作的是拷贝副本,不是原容器。真正修改容器行为反而不可控、不清晰。

缺点(致命):

缺点说明
总是复制容器Q_FOREACH 会拷贝一份容器(无论你愿不愿意),即使你只读。对 STL 容器非常昂贵
拷贝的是 const 容器无法修改容器元素(因为元素是 const)
可读性差隐式语义不清楚,容易出 bug
与现代 C++ 不兼容不支持 STL 容器,不支持迭代器,不支持 structured bindings 等
将在 Qt 6 中被移除官方明确计划废弃此功能

最佳实践

  • 使用 C++11 的 range-based for
for (const auto& item : container) {// 安全、高效、清晰
}
  • 禁用 Q_FOREACH / foreach
    在项目中定义:
DEFINES += QT_NO_FOREACH  // for qmake
# 或
add_definitions(-DQT_NO_FOREACH)  // for CMake

这将使编译器在你使用 foreach直接报错,强制你使用现代 C++。

额外建议

替换建议替代语法
foreach (auto x, list)for (const auto& x : list)
foreach (QString s, strings)for (const QString& s : strings)
foreach (int i, QVector<int>)for (int i : QVector<int>)
如果你愿意,我可以:
  • 扫描一个项目中的所有 foreach 并一键转换为 C++11 语法;
  • 或者写一个 clang-tidy 规则 / Python 脚本帮助自动替换。

这一节讲的是 range-based for 循环(基于范围的 for 循环) 在 Qt 和 STL 容器上的行为细节,尤其是它可能引发的 隐式分离(implicit detach) 问题。

Range-based for 的真实展开形式:

for (var : container) body;

等价于

{auto &&c = (container);auto i = begin(c);auto e = end(c);for (; i != e; ++i) {var = *i;body;}
}

STL 容器(如 std::vector<T>)的行为

  • iestd::vector<T>::iterator
  • 如果你不在 body 中修改元素或容器,没有副作用
  • 这也是现代 C++ 推荐的方式。

Qt 容器(如 QVector<T>)的行为

  • ieQVector<T>::iterator不是 const_iterator
  • 即使你不在 body 中修改容器,也可能导致 隐式 detach

    因为 Qt 容器在调用 non-const 成员函数(如 begin()end())时,如果 refcount > 1,会触发深拷贝(detach)。

例如:

QVector<QString> v = ...;
for (const auto& s : v) {qDebug() << s;
}

乍一看没问题,但 v.begin()v.end()非 const 成员函数,可能导致:

  • 性能开销:触发 deep copy
  • 行为变化:影响共享数据的其它副本

正确做法(避免 detach)

推荐方式:明确使用 const&const_iterator

// 使用 const 引用,避免 detach
for (const QString& s : v) {qDebug() << s;
}

或者,如果你写模板代码,优先使用 const QVector<T>& 参数,这样 begin() 会是 const_iterator,避免 detach。

小结:如何安全使用 range-based for?

情况建议
使用 STL 容器直接使用 range-based for,安全高效
使用 Qt 容器容器变量加 const&,元素加 const&
不确定是否 detachconst_iterator 避免陷阱

这一部分是对 Qt 的 Q_FOREACH 和 C++11 的 range-based for loop 在使用 Qt 容器和 STL 容器时的行为差异总结。下面帮你归纳一下关键点:

range-based for vs Q_FOREACH 对比总结

容器类型Q_FOREACHrange-based for (auto & : c)range-based for (const auto & : c)
Qt 非 constOK(cheap)可能会 detach(非 const 迭代器)可能会 detach(begin() 不是 const)
Qt constOK(cheap)不会 detach(const 迭代器)不会 detach(const 迭代器)
STL 非 const会 deep copy(复制一份)OKOK
STL const会 deep copyOKOK

为什么有这些区别?

Q_FOREACH 的问题:

  • 始终复制容器(哪怕是 const 容器),对于 STL 容器来说是灾难性的(深拷贝)。
  • 对 Qt 容器没什么问题,因为 Qt 使用了 implicit sharing,所以复制是廉价的。
  • 缺点是代码难以推理,性能不透明,因此 Qt 6 已废弃 Q_FOREACH

range-based for 的细节:

for (auto &item : container) {// ...
}
  • 如果 containerQt 非 const 容器
    • 调用的是 begin()end(),它们是 非常量成员函数
    • 如果容器被共享(refcount > 1),会发生 detach(深拷贝)。
  • 如果 containerconst,就会使用 const_iterator,不会触发 detach。

实践建议(写 Qt 代码时):

  1. 避免使用 Q_FOREACH,在项目中定义:
    #define QT_NO_FOREACH
    
  2. 优先使用 range-based for,但要注意:
    const QVector<int> vec = ...;
    for (const auto &x : vec) {  //  安全,不会 detach...
    }
    QVector<int> vec = ...;
    for (auto &x : vec) {        //  可能 detach(如果被共享)...
    }
    
  3. 如果一定需要修改容器或元素,考虑:
    • 保证容器未共享;
    • 或使用 detach() 手动控制。
  • 非 const Qt 容器使用 range-based for 循环时,要小心可能触发隐式 detach
  • 如果不修改容器,尽量用 const 容器或者通过 qAsConst()(Qt5.7起)或 std::as_const()(C++17起)将容器转换为 const
  • 不能对临时(rvalue)直接使用 qAsConst(),这种情况下先用 const 引用绑定,再循环。
    这样可以避免不必要的深拷贝,提升性能,且代码更安全。

Clazy,它是基于 Clang 的开源静态分析工具,专门针对 Qt 代码。总结一下:

  • Clazy 类似于 clang-tidy,但聚焦于 Qt 风格和 Qt 特有的坑。
  • 它自带 50+ 规则检查,比如:
    • detaching-temporary(检测隐式 detach 相关问题)
    • strict-iterators(检测迭代器使用)
    • missing-typeinfo(缺少类型信息)
    • foreach(检测不建议用的 Qt foreach)
  • 它还能自动提供 fix-it,帮你自动改代码。
  • 即使是 Qt 自己的代码库,也有不少问题被 Clazy 检测出来。
  • 建议定期用 Clazy 扫描你的代码,修复警告,提升代码质量和性能。
    这工具对保持 Qt 代码库的健康和现代化很重要,尤其是避免隐式 detach 等细节导致的性能问题。

Qt 字符串类创建方式

常见的创建字符串的方式很多:

  1. 直接字符串字面量 "string"
  2. QByteArray("string") —— 字节数组,无编码信息
  3. QByteArrayLiteral("string") —— 编译时常量,不分配内存
  4. QString("string") —— UTF-16编码字符串,分配内存
  5. QLatin1String("string") —— 轻量视图,适合拉丁1编码
  6. QStringLiteral("string") —— 编译时UTF-16常量,不分配内存(Qt 5.9+)
  7. QString::fromLatin1("string") —— 从Latin1编码构造
  8. QString::fromUtf8("string") —— 从UTF-8编码构造
  9. tr("string") —— 用于国际化的字符串
  10. QStringView(u"string") —— 轻量视图,不分配内存

QByteArray

  • 表示字节序列,类似 std::string,不包含编码信息
  • 隐式共享(copy-on-write)
  • 构造函数会分配内存
  • QByteArray::fromRawData() 可以避免部分分配
  • QByteArrayLiteral() 不分配内存,存储于只读段,适合静态数据

QString

  • 使用 UTF-16 编码,支持 Unicode 操作(优于 std::u16string
  • 隐式共享
  • 构造函数会分配内存
  • QString::fromRawData() 可以避免分配(只读视图)
  • 推荐使用 QStringView 作为轻量字符串视图
  • QStringLiteral() 自 Qt 5.9 起不分配内存,数据存储在只读段,适合字符串常量
    总结:
  • 用 Qt 来管理 Unicode 字符串,选 QString;如果只读且想避免拷贝,选 QStringViewQStringLiteral
  • QByteArray 处理原始字节流或二进制数据。
    “Latin1” 是“ISO 8859-1”编码的简称,全称是 ISO/IEC 8859-1: Latin Alphabet No. 1
    简单来说:
  • 它是一种单字节字符编码,使用 1 个字节(8 位)表示一个字符;
  • 能表示西欧主要语言的字符集,比如英语、法语、德语、西班牙语等;
  • 范围覆盖了 0x00 到 0xFF 共 256 个字符,其中前 128 个字符和 ASCII 码完全一样;
  • 它不支持像中文、日文、韩文等复杂字符,只适合基本拉丁字母和西欧符号;
  • 在 Qt 里,QLatin1String 是对 Latin1 编码字符串的轻量包装,用来高效处理这类字符串,避免转码成本。
    总结:Latin1 是一种旧式的、西欧字符编码,适合只包含拉丁字母的文本,不支持多语言 Unicode。

简单总结一下 QLatin1String 的作用和特点:

  • 它是一个轻量的字符串包装类,只包含一个 const char* 指针和字符串长度,不做内存管理;
  • 主要用来表示 Latin1(ISO 8859-1)编码的字符串字面量,比如代码里的 "foo"
  • 用于 QString 相关函数的重载,避免不必要的临时 QString 分配和转换,提高性能;
  • 例如:QString::startsWith() 同时有两个版本,一个接受 QString,一个接受 QLatin1String,后者性能更好,因为不产生临时字符串。
    你可以把它看成是 Qt 里对纯 ASCII 或 Latin1 字符串字面量的一个“快捷通道”,用来减少字符串转换和内存开销。

总结一下这段内容的重点:

  • Qt 的主要字符串类是 QStringQByteArray
  • 这几年对它们的改进不多,保持了比较稳定的设计。
  • 从 Qt 5.9 开始,QStringLiteralQByteArrayLiteral 这两个宏的实现优化了——它们不会再动态分配内存,而是直接使用编译时生成的静态内存,这样可以显著提升性能。

简单总结一下 QStringView

  • 从 Qt 5.10 引入的类型,是一个 非拥有(non-owning) 的字符串视图,类似于 C++17 标准的 std::u16string_view
  • 它直接指向一段 UTF-16 编码的字符序列(比如 QString 内部存储格式),但不负责管理这段内存。
  • 这样可以避免不必要的字符串拷贝,提升性能,特别适合只读访问场景。
  • 它提供了和 QString 大部分相似的只读接口,方便使用。
  • Qt 5.11 之后还会有更多的 API 和对 QStringBuilder 的支持,使用体验会更好。

这段讲的是用 QStringView 作为函数参数类型的理由和好处,重点如下:

  • 主要用途QStringView 适合作为函数参数,尤其是函数需要读取字符串但不需要保留它时。
  • 如果函数需要一个 Unicode 字符串参数,而且函数不会保存这个字符串,推荐用 QStringView,避免无谓的拷贝和分配。
  • 讨论了一个例子:Document::find(StringType substring)StringType 用哪个类型好?
    • QString 会强制调用者提供一个完整的 QString,可能要动态分配内存,或者使用 QStringLiteral 编译期字符串。
    • QByteArray 不是 Unicode 安全的,不适合处理 Unicode 文本。
    • QLatin1String 虽然性能好(因为是 Latin-1 编码),但不 Unicode 安全,不过可以做为额外重载实现快速路径。
      总结:QStringView 既支持 Unicode,也避免了字符串不必要的复制,非常适合作为 API 中接受字符串参数的接口类型。

这部分内容强调了 QStringView 作为接口类型的优势:

  • QStringView 是 Unicode 安全的(支持 UTF-16 编码)。
  • 它不会进行内存分配(alloc-free),性能好。
  • 它可以从多种字符串源构造:
    • 编译时的字符串字面量(u"compile time"
    • 动态分配的 QString 对象
    • 甚至是一个大字符串的子串(通过 QStringView(bigString, 40)
      举例:
class Document {iterator find(QStringView substring);
};
Document d;
d.find(u"compile time");          // 传入编译时字符串字面量
QString allocatedString = "...";
d.find(allocatedString);          // 传入动态分配的QString
d.find(QStringView(bigString, 40)); // 传入大字符串的子串视图

另外,QStringView 还能作为“零分配”切割字符串的工具,比如:

QString str = "...";
QRegularExpression re("reg(.*)ex");
QRegularExpressionMatch match = re.match(str);
if (match.hasMatch()) {QStringView cap = match.capturedView(1); // 直接获取子串视图,无需分配内存// ...
}

总结:QStringView 让字符串处理既高效又安全,非常适合作为函数参数和字符串子串的视图类型。

这一部分讲的是 QStringView 对 Qt API 的巨大影响:

  • 许多 Qt 函数现在接受 QString 参数,但实际上并不需要持有字符串数据,只是读取它们。
  • 在 Qt 6 中,应该将这些函数改为接受 QStringView,这样避免不必要的内存分配和拷贝,提高性能。
  • Qt 5 里还有一个类似的非拥有字符串视图类型叫 QStringRef,但它设计有缺陷:
    • 它必须绑定到一个 QString 对象,而不能直接表示任何 UTF-16 字符序列。
    • 因此灵活性较差。
  • QStringRef 只是权宜之计,建议如果迫切需要用字符串视图,可以暂时用它,但随着 QStringView 在功能上达到 API 完整性,QStringRef 会被废弃。
    总结:QStringView 是更现代、更高效的字符串视图接口,未来 Qt 版本将以它为标准,替代旧的字符串引用方式。

POD(Plain Old Data) 平凡数据类型 类型是否等同于“可搬移”(relocatable)?

  • 答案是否定的:POD和可搬移是两个独立的概念。
  • 一个类型是否可搬移(relocatable)与它是否是POD类型无关。
  • 可搬移类型可能有非平凡的构造函数和析构函数,比如 Qt 里基于pimpl(指针实现)的值类。
  • 反过来,即使是平凡(trivial)的类型,也未必可搬移——比如某些类型的对象地址本身代表身份(identity),搬移会破坏语义。
  • 所有C数据类型都是trivial,但不一定是relocatable。
    另外,关于Qt中废弃的API(deprecated APIs)
  • Qt会标记旧的API为废弃,虽然它们仍然可用且通过测试,但Qt 6版本会移除大部分废弃API。
  • Qt源码中通过宏 QT_DEPRECATED_SINCE(major, minor) 来标记,并用 QT_DEPRECATED_X("建议替代方法") 生成编译警告。
  • 你可以通过定义宏来控制废弃API的使用:
    • 定义 QT_DEPRECATED_WARNINGS 来开启废弃API的警告。
    • 定义 QT_DISABLE_DEPRECATED_BEFORE=版本号 来将早于某版本的废弃API使用视为错误。
      例如:
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x050900

这样就强制不允许使用 Qt 5.9.0 及更早版本中废弃的API。
总结:

  • POD ≠ relocatable,二者语义不同。
  • Qt鼓励逐步迁移,避免使用废弃API,尤其是升级到Qt 6时。

QList 的核心问题是:

  • 它是一个“基于数组”的容器,但内部实现是一个 void* 指针数组。
  • 根据存储的类型不同,QList 可能存放指向元素的指针(每个元素单独堆分配),也可能直接存放元素本身。
  • 这种设计导致:
    • 每个元素单独堆分配时性能和内存效率都很差。
    • 元素存储方式依赖于平台(32位 vs 64位)和元素类型,行为不稳定难以预测。
    • 对于小且可搬移的数据类型(如 int 在64位平台)非常浪费空间。
  • QList 优化了前置插入操作(prepend),但代价较大。
  • 尽管有这些问题,QList仍然是Qt API中最常暴露的容器之一。
    总结:
    不要在自己的代码中使用 QList,推荐使用 QVector 或 STL 容器,除非必须和 Qt API 交互。

总结下 QList 和 QVector 的区别和使用建议:

  • 推荐使用 QVector,除非你必须调用需要 QList 的 Qt API。
  • QVector 通常生成更少的代码,性能也更好。
  • QVector 在大多数操作上比 QList 快,唯一例外是:
    • 经常在前面插入元素时,QList表现可能更好。
    • 对非常大的对象进行重新分配时,QList可能更合适。
  • QVector 重新分配时,不保证引用或指针的有效性(会失效)。
  • 如果需要引用或指针保持有效,建议用指针的容器,比如 QVector<T*>
    简单说就是:

绝大多数情况下用 QVector,只有少数场景考虑 QList。

http://www.lryc.cn/news/572073.html

相关文章:

  • 锂电池保护板测试仪:守护电池安全的幕后保障
  • 【css】设置了margin-top为负数,div被img覆盖的解决方法
  • django调用 paramiko powershell 获取cpu 个数
  • IPv4编址及IPv4路由基础
  • Pinia + Vue Router 权限控制(终极完整版)
  • 无监督学习中的特征选择与检测(FSD)在医疗动线流程优化中的应用
  • 2025-05-05-80x86汇编语言环境配置
  • 使用随机森林实现目标检测
  • AI时代SEO关键词革新
  • 医疗低功耗智能AI网络搜索优化策略
  • 49-Oracle init.ora-PFILE-SPFILE-启动参数转换实操
  • 129. 求根节点到叶节点数字之和 --- DFS +回溯(js)
  • 详解鸿蒙Next仓颉开发语言中的全屏模式
  • 【hadoop】搭建考试环境(单机)
  • LVS+Keepalived+nginx
  • Spring Boot + MyBatis + Vue:打造高效全栈应用的黄金组合
  • Vue 组件数据流与状态控制最佳实践规范
  • 博图SCL中CONTINUE语句详解:高效循环控制案例
  • RabbitMQ多角度可靠性分析/基于Java代码深度解析
  • android 象棋游戏开发
  • Android Studio Profiler使用
  • 数据差异的iOS性能调试:设备日志导出和iOS文件管理
  • Redis之分布式锁(3)
  • 【深度学习】条件随机场(CRF)深度解析:原理、应用与前沿
  • Ubuntu 安装Telnet服务
  • Cursor Pro取消500次请求限制,无限用的体验更好了吗?
  • YOLOv8改进:Neck篇——2024.1全新MFDS-DETR的HS-FPN特征融合层解析
  • 图像特征检测算法SIFT
  • 实现自动胡批量抓取唯品会商品详情数据的途径分享(官方API、网页爬虫)
  • python校园拼团系统