QList 上基于 C++11 范围的循环中的 "container detachement" 是什么?这只是性能问题吗?

What is a "container detachement" in the C++11 ranged based loop over QList? Is it a performance only problem?

包含一些解决问题的建议,我想更深入地了解问题的确切原因:

 QList<QString> q;
 for (QString &x: q) { .. }
  1. 是不是这样,除非容器被声明const,否则Qt会做一个 列表的副本然后遍历该副本?这不在 最好的,但如果列表很小(比如 10-20 QString 的).
  2. 这只是性能问题还是更深层次的问题 问题?假设我们没有 add/remove 个元素,而循环是 运行.
  3. 是循环中对值的修改(假设是 参考)仍然有效的东西或者它从根本上是 坏了?

Copy-on-write(=隐式共享)概念

重要的是要了解 copy-on-write (= implicit shared) 类 的外部行为与执行数据深拷贝的“正常”类 一样。他们只会尽可能长时间地推迟这种(可能)昂贵的复​​制操作。仅当出现以下序列时才会进行深层复制(=分离):

  • 列表是隐式共享的,即对象按值复制(并且至少有 2 个实例仍然存在)
  • 在隐式共享对象上访问了 non-const 成员函数。

您的问题

  1. 只有当容器被共享时(由这个列表的另一个写入实例复制),列表的一个副本将是made(因为在列表对象上调用了 non-const 成员)。请注意,C++ 范围循环只是基于 for 循环的普通迭代器的简写(请参阅 [1] 了解确切的等价性,这取决于确切使用的 C++ 版本):

    for (QList<QString>::iterator& it = q.begin(); x != q.end(); ++it)
    {
        QString &x = *it;
        ...
    }
    

    请注意,当且仅当列表 q 本身被声明为 const 时,begin 方法是一个 const 成员函数。如果你自己写完整的,你应该使用 constBeginconstEnd 来代替。

    所以,

    QList<QString> q;
    q.resize(10);
    QList<QString>& q2 = q; // holds a reference to the same list instance. Modifying q, also modifies q2.
    for (QString &x: q) { .. }
    

    不执行任何复制,因为列表 q 未与另一个实例隐式共享。

    然而,

    QList<QString> q;
    q.resize(10);
    QList<QString> q2 = q; // Copy-on-write: Now q and q2 are implicitly shared. Modifying q, doesn't modify q2. Currently, no copy is made yet.
    for (QString &x: q) { .. }
    

    确实复制了数据。

  2. 这主要是性能问题。仅当列表包含一些具有 奇怪副本 constructor/operator 的特殊类型时,情况可能并非如此,但这可能表明设计不当。在极少数情况下,您可能还会遇到 Implicit sharing iterator problem,方法是在迭代器仍处于活动状态时分离(即深拷贝)列表。

    因此,在任何情况下避免不必要的副本都是一种很好的做法,方法是:

    QList<QString> q = ...;
    for (QString &x: qAsConst(q)) { .. }
    

    const QList<QString> q = ...;
    for (QString &x: q) { .. }
    
  3. 循环中的修改没有中断并按预期工作,即它们的行为就好像 QList 不使用隐式共享,但在复制 constructor/operator 期间执行深层复制。例如,

    QList<QString> q;
    q.resize(10);
    QList<QString>& q2 = q;
    QList<QString> q3 = q;
    for (QString &x: q) {x = "TEST";}
    

    qq2 相同,都包含 10 次“TEST”。 q3 是一个不同的列表,包含 10 个空 (null) 字符串。

同时检查 Qt 文档本身关于 Implicit Sharing,它被 Qt 广泛使用。在现代 C++ 中,这种性能优化结构可以(部分)替换为新引入的移动概念。

通过检查源代码加深理解

每个 non-const 函数调用 detach,在实际修改数据之前,f.ex。 [2]:

inline iterator begin() { detach(); return reinterpret_cast<Node *>(p.begin()); }
inline const_iterator begin() const noexcept { return reinterpret_cast<Node *>(p.begin()); }
inline const_iterator constBegin() const noexcept { return reinterpret_cast<Node *>(p.begin()); } 

然而,detach仅在实际共享列表时才有效detaches/deep复制数据[3]:

inline void detach() { if (d->ref.isShared()) detach_helper(); }

isShared实现如下[4]:

bool isShared() const noexcept
{
    int count = atomic.loadRelaxed();
    return (count != 1) && (count != 0);
}

即存在超过 1 个副本(= 除了对象本身之外的另一个副本)。