Qt Container

Table of Contents

1. Qt Container

1.1. Overview

Qt 的 container 与 Rust Box 类似, 它们本身只是一个 wrapper, 真正的数据是在堆上分配的.

这种 wrapper 有两个好处:

  1. shadow copy, 复制一个 container 的开销很小
  2. copy on write, 只在需要的时候才 copy 堆上的数据

每种 container 底层都有一个 XxxData 代表堆上数据, 同时包含引用计数负责堆上数据的释放, 另外, 对于所有可能修改堆上数据的操作, 都会通过 detach 触发 copy on write

1.1.1. shadow copy

// case 1:
QVector<int> data {1, 2, 3};
auto data2 = data;

// case 2:
QVector<int> getData() {
    return QVector<int> {1, 2, 3};
}
auto data = getData();

// case 3:
void check(QVector<int> data) {}
check(QVector<int> {1, 2, 3})

上面三种情况下, data 都是 shadow copy, 其底层的 QTypedArrayData 会指向相同的堆上数据

QVector<int> data {1, 2, 3};
printf("%p\n", &data[0]);

const auto data2 = data;
printf("%p\n", &data2[0]);

// 输出为:
// 0x1191e68
// 0x1191e68

1.1.2. copy on write

int main(int argc, char *argv[]) {
    QVector<int> data {1, 2, 3};
    printf("%p\n", &data[0]);

    auto data2 = data;
    printf("%p\n", &data2[0]);

    return 0;
}

// 输出为:
// 0x1219e68
// 0x1219e98

虽然 data2 并没有显式的修改数据, 但从 qt 看来, 还是有可能发生潜在的 write: 因为 data2 通过 operator[] 返回了堆上数据的引用, 为了防止代码在别处修改这个引用的数据导致的 write, qt 会针对非 const 的 operator[] 进行 copy

1.2. shadow copy

template <typename T>
inline QVector<T>::QVector(const QVector<T> &v)
{
    // 若 ref() 为 true, 表示 v 是 shareable 的, 则通过
    // ref() inc 引用计数后, 直接让 this.d = v.d 即可,
    // 其中 d 即堆上的 QTypedArrayData
    if (v.d->ref.ref()) {
        d = v.d;
    } else {
        if (v.d->capacityReserved) {
            d = Data::allocate(v.d->alloc);
            Q_CHECK_PTR(d);
            d->capacityReserved = true;
        } else {
            // d 是在堆上分配的
            d = Data::allocate(v.d->size);
            Q_CHECK_PTR(d);
        }
        if (d->alloc) {
            copyConstruct(v.d->begin(), v.d->end(), d->begin());
            d->size = v.d->size;
        }
    }
}

shadow copy 是通过 RefCount 实现的, 和 c++ 的 shared_ptr 类似.

1.2.1. setSharable

默认情况下 container 都是 shareable 的, 拷贝构造函数会是 shadow copy. 通过 setSharable(false), 可以阻止这种行为.

1.3. copy on write

  • 根据 RefCount 决定是否需要通过 detach 进行 copy
  • 针对所有可能修改对象的方法进行 copy on write, 包括显式的 remove, insert 等操作, operator [] 也有可能导致 detach. 理论上, 所有非 const 方法都应该 detach

1.3.1. removeLast

template <typename T>
void QVector<T>::removeLast()
{
    if (d->ref.isShared())
        detach();
    --d->size;
    if (QTypeInfo<T>::isComplex)
        (d->data() + d->size)->~T();
}

detach 会负责:

  1. 将原始的 d deref, 即减小原来对象的引用计数
  2. 通过 realloc 分配新的内存, 复制旧对象的所有数据(而非仅仅是修改的数据)

1.3.2. operatror []

operator [] 会导致 detach:

inline T &QVector<T>::operator[](int i) {
    return data()[i];
}

inline T *data() { detach(); return d->begin(); }

const operator [] 并不需要 detach:

template <typename T>
inline const T &QVector<T>::operator[](int i) const {
    return d->begin()[i];
}

1.4. detach 导致的问题

1.4.1. 隐含的 detach

int main(int argc, char *argv[]) {
    QVector<int> data {1, 2, 3};
    printf("%p\n", &data[0]);

    auto dataCopy = data;
    for (int &i : dataCopy) {
        printf("%p\n", &i);
    }
    return 0;
}

// 0x14f3e68
// -------
// 0x14f3e98
// 0x14f3e9c
// 0x14f3ea0
// 因为 dataCopy 进行遍历时, 隐含的使用了 dataCopy.begin() 方法,
// 而非 const 的 iterator 会隐含着 detach

为避免隐含的 detach, 应尽量使用 const 对象

1.4.2. 修改的不一致

QVector<int> data{1, 2, 3};
int &first = data[0];

auto data2 = data;
first = 2;

foreach(int i, data2) {
    printf("%d\n", i);
}
// 2
// 2
// 3
// 
// 虽然 data2 可以透明的看作是 data 的 deep copy, 但其 data2[0] 还是被修改了

之所以发生问题, 是因为 qt 无法针对 first = 2 这种操作自动 detach

1.5. foreach

1.5.1. foreach 宏会隐式的在 data 的 copy 上迭代:

QVector<int> data{1, 2};
data.reserve(100);

for(int &i : data) {
    printf("%p %d\n", &i, i);
}
printf("---\n");
// foreach 会隐式在 data 的 copy 上迭代
// 所以 foreach 里可以自由的修改 data, 而且修改会导致 detach
foreach (int i, data) {
    data.insert(0, i);
}

for(int &i : data) {
    printf("%p %d\n", &i, i);
}

printf("---\n");

// range-based loop 并不会对 data 做 copy, 本质上它是基于 iterator 的, 它对 data 修改是不可预测的
for(int &i : data) {
    data.insert(0, i);
}

for(int &i : data) {
    printf("%p %d\n", &i, i);
}
// 0xd90438 1
// 0xd9043c 2
// ---
// 0xd79e68 2
// 0xd79e6c 1
// 0xd79e70 1
// 0xd79e74 2
// ---
// 0xd79e68 2
// 0xd79e6c 2
// 0xd79e70 2
// 0xd79e74 2
// 0xd79e78 2
// 0xd79e7c 1
// 0xd79e80 1
// 0xd79e84 2

1.5.2. foreach 在 data 的 const copy 上迭代

由于 foreach 操作的实际是原数据的 copy, 所以修改 iterator 是没有意义的, 因此, 这里的 copy 实际上是 const copy

foreach (int &i, data) {
    i += 1;
}
// /home/ANT.AMAZON.COM/waysun/program/qt/5.14.0/gcc_64/include/QtCore/qglobal.h:1110:34: error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
//      for (variable = *_container_.i; _container_.control; _container_.control = 0)

这里的 i 是 const int, 无法被修改

1.6. c++ range-based for loop

range-based for 是通过 iterator 直接操作数据:

  1. 不需要 copy
  2. 可以修改 iterator
  3. 不可以修改 container
  4. begin() 会隐含一个 detach
QVector<int> data{1, 2, 3};
for(int &i : data) {
    printf("%p %d\n", &i, i);
}
printf("---\n");
auto data2 = data;
for(const int &i : qAsConst(data2)) {
    printf("%p %d\n", &i, i);
}
printf("---\n");
for(int &i : data2) {
    printf("%p %d\n", &i, i);
}
printf("---\n");
for(int &i : data2) {
    i += 1;
}
for(int &i : data2) {
    printf("%p %d\n", &i, i);
}
// 0x65ee68 1
// 0x65ee6c 2
// 0x65ee70 3
// ---
// 0x65ee68 1
// 0x65ee6c 2
// 0x65ee70 3
// ---
// 0x65ee98 1
// 0x65ee9c 2
// 0x65eea0 3
// ---
// 0x65ee98 2
// 0x65ee9c 3
// 0x65eea0 4

1.7. foreach 与 for(;) 的比较

  • foreach 会复制, 可以修改原 container, 由于使用了 const copy, 所以 iterator 不能修改, 但 iterator 也不会导致 detach
  • ranged-based for 不复制, 不可以修改原 container, 由于使用了 iterator, 所以 iterator 可以修改, 但 iterator 也会导致 detach
  • foreach (QPair<int,int> p, list) 无法编译通过, 因为宏无法自理多余的 `,`
  • foreach 隐含的复制行为, 可以会引起不注意的 bug
  • for(;) 虽然会导致 detach, 但通过 qAsConst 可以避免
  • foreach 已经 deprecated

1.8. Recap

  • 通过 setSharable 可以控制是否使用 implicit sharing
  • 由于 container 使用 RefCount 实现了 shadow copy 和 copy on write, 通常不再需要在堆上分配 Container, 直接在栈上分配即可. qt 保证了底层数据是在堆上分配的
  • 使用 const 对象或 qAsConst 避免隐含的 detach
  • foreach 与 for(;) 的区别

Author: [email protected]
Date: 2020-01-07 Tue 00:00
Last updated: 2021-01-28 Thu 21:41

知识共享许可协议