Skip to content

Qt 设计模式:享元模式与隐式共享 (COW)

系列:Qt / VTK 设计模式 · Qt 09/11
参考:Qt 6 Implicit SharingQSharedDataPointer


引子

如果程序中有 10000 个控件各持有一张相同的背景图片,若每个控件都深拷贝一份像素点阵数组,内存将会迅速被吃满崩溃。但在 Qt 中,把一个包含千万像素的 QImageQPixmap 赋值给另一个变量,时间复杂度是 ,内存开销仅增加一个指针。

这就是**享元模式(Flyweight Pattern)在 Qt 中的落地:运用共享技术有效地支持大量细粒度对象的复用。Qt 将享元模式与值类型语义结合,演化出了闻名遐迩的隐式共享(Implicit Sharing / Copy-On-Write 写时复制)**技术。


核心痛点与 GoF 享元结构

痛点场景:大量重复对象的内存风暴

cpp
// 坏方案:频繁进行像素点阵或数组数据的深拷贝
QImage img1("huge_photo.png"); // 占用 50MB 内存
QImage img2 = img1;            // 如果是深拷贝,立刻额外占用 50MB 内存!
  • 内存爆炸:大尺寸数组、点云数据、图像矩阵在多次传参和拷贝时引发严重的内存冗余。
  • 性能低下:分配海量内存并进行内存块复制耗时剧烈。

GoF 享元模式结构与 Qt 对应映射

享元模式的核心是将对象状态划分为 内部状态 (Intrinsic State)外部状态 (Extrinsic State)

享元模式概念职责说明Qt 隐式共享实现 (QSharedData)
内部状态 (Intrinsic State)独立于环境、可全局共享且不可变重数据存储在底层引用计数块(如 QImageData 像素点阵)中
外部状态 (Extrinsic State)随环境变化、由客户端维护的轻量数据浅拷贝句柄对象本身(如 QImage 壳对象的尺寸、坐标引用)
享元工厂 / 享元句柄维护并分配共享资源句柄QSharedDataPointer<T> / QExplicitlySharedDataPointer<T>

底层原理:写时复制 (Copy-On-Write / COW) 流程

Qt 隐式共享对象的内部结构分为“外壳句柄类 (Value Host)”与“底层引用计数数据块 (Shared Data)”:

  1. 浅拷贝(读共享):执行 b = a 时,只复制指向 QSharedData 的指针并原子递增 ref 计数,耗时极短。
  2. 写时分裂 (detach()):当尝试对 b 进行写入修改(如 b.bits() 或修改像素)时,b 检查到 ref > 1,立刻触发 detach() 申请新内存完成深拷贝,使 a 的数据完全不受影响。

Qt 隐式共享类与 Qt 6 变迁

经典隐式共享类

  • 图像与图形类QImage, QPixmap, QBitmap, QFont, QPen, QBrush
  • 容器与字节流QByteArray, QList, QVector, QHash, QMap

Qt 6 中的 QString 演进重点

在 Qt 5 中,QString 广泛采用了 COW 机制;但从 Qt 6 开始,QString 默认取消了 COW,转而采用 SSO(Small String Optimization 小字符串优化) 和强推荐使用 QStringView / std::u16string_view。这是因为在现代 CPU 架构下,多线程频繁原子增减 ref 计数的开销有时甚至超出了小字符串直接内存拷贝的成本。


完整代码示例:自定义一个隐式共享 (COW) 享元类

Qt 提供了 QSharedDataQSharedDataPointer 帮助开发者用极简的代码打造符合写时复制语义的自定义类:

cpp
#include <QSharedData>
#include <QSharedDataPointer>
#include <QString>
#include <QDebug>

// 1. 定义底层内部共享数据块 (Intrinsic State)
class HugePointCloudData : public QSharedData {
public:
  HugePointCloudData() = default;
  HugePointCloudData(const HugePointCloudData& other)
    : QSharedData(other), m_points(other.m_points) {
    qDebug() << "--- Physical Deep Copy PointCloud Memory ---";
  }
  ~HugePointCloudData() = default;

  QList<float> m_points; // 假设包含百万级点云坐标
};

// 2. 定义面向用户的轻量享元外壳类 (Extrinsic Host)
class PointCloud {
public:
  PointCloud() : d(new HugePointCloudData) {}

  // 增加点数据(会触发写时分离)
  void addPoint(float p) {
    // d.detach() 会自动检测 refCount,若 > 1 则调用 HugePointCloudData 拷贝构造函数分裂
    d->m_points.append(p); 
  }

  int count() const {
    // 只读访问不会触发 detach
    return d->m_points.size();
  }

private:
  QSharedDataPointer<HugePointCloudData> d; // 自动化 COW 智能指针
};

int main() {
  PointCloud cloudA;
  cloudA.addPoint(1.0f);
  cloudA.addPoint(2.0f);

  // 浅拷贝:零内存深拷贝开销,仅指针传递与引用计数+1
  PointCloud cloudB = cloudA; 
  qDebug() << "Cloud A Count:" << cloudA.count(); // 2
  qDebug() << "Cloud B Count:" << cloudB.count(); // 2

  // 触发修改:cloudB 内部自动调用 detach() 独立分裂
  cloudB.addPoint(3.0f); 
  
  qDebug() << "After Detach Cloud A Count:" << cloudA.count(); // 2
  qDebug() << "After Detach Cloud B Count:" << cloudB.count(); // 3 (独立的物理数据)

  return 0;
}

易混淆模式对比

设计模式核心目的共享机制与状态
享元模式 (Flyweight)减少细粒度对象内存分离内部/外部状态,只读共享内部重数据,写入时分离 (COW)
单例模式 (Singleton)保证全局唯一实例全局只有一个对象句柄,没有写时分离的概念
原型模式 (Prototype)通过克隆创建对象每次克隆通常直接进行独立的深拷贝

最佳实践与踩坑避坑

  1. 传递隐式共享参数尽量使用 const 引用
    • 函数传参如 void processImage(const QImage& img)。如果直接传值 void processImage(QImage img),尽管拷贝构造非常快,但在函数体内如果误调了非 const 方法,会意外触发 detach() 引发无谓的深拷贝。
  2. 多线程并发安全规则
    • 隐式共享对象的引用计数增减是原子线程安全的。多个线程同时只读访问同一个 QImage 是完全安全的;但如果有线程尝试写入修改该共享对象,必须在外部加锁同步,防止在 detach() 过程中产生竞态。
  3. 性能敏感热路径避免非 const 迭代器
    • QList / QByteArray 使用非 constoperator[]begin() 会隐式触发 detach()!即使你只是读取。只读遍历请务必使用 cbegin() / cend()at()

重点与注意

重点:享元模式在 Qt 中的核心体现是隐式共享(COW 写时复制):浅拷贝极快,非必要不深拷贝。
重点:使用 QSharedDataQSharedDataPointer 可以非常优雅地为自定义大对象实现 COW 写时分离。
注意:非 const 方法(如 operator[]bits())会主动触发 detach(),只读操作请使用 at()const 引用。


小结

Qt 享元模式(隐式共享)优雅地平衡了 C++ 值语义的易用性与引用传递的高性能,是写出高性能 Qt 数据的利器。

延伸阅读

基于 VitePress 强力驱动 | 记录技术与生活