QList 上的 C++11 基于范围的循环中的“容器分离”是什么?这只是性能问题吗?

问题描述 投票:0回答:1

这个问题包含一些解决该问题的建议,我想更深入地了解问题到底是:

 QList<QString> q;
 for (QString &x: q) { .. }
  1. 是不是这样,除非容器被声明
    const
    ,Qt 会制作一个 列表的副本,然后迭代该副本?这不在其中 最好的,但如果列表很小(比如 10-20 QString 的)。
  2. 这只是性能问题还是可能有更深层次的问题 问题?假设我们在循环运行时不添加/删除元素。
  3. 是循环中数值的修改(假设是 参考)仍然有效或从根本上有效的东西 坏了?
c++ qt foreach copy-on-write
1个回答
7
投票

写时复制(=隐式共享)概念

重要的是要理解写时复制(=隐式共享)类的外部行为类似于执行数据(深层)复制的“普通”类。他们只是尽可能推迟这个(可能)昂贵的复制操作。仅当发生以下序列时,才会进行(深层)复制(=分离):

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

您的问题

  1. 仅当容器被共享时(通过此列表的写入实例的另一个副本),将创建列表的副本(当在列表对象上调用非常量成员时)。请注意,C++ 范围循环只是基于 for 循环的普通迭代器的简写(请参阅 [1] 了解确切的等效项,这取决于确切使用的 C++ 版本):

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

    请注意,当且仅当列表

    begin
    本身声明为 const 时,
    q
    方法才是 const 成员函数。如果您自己完整编写,则应该使用
    constBegin
    constEnd
    来代替。

    那么,

    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. 这主要是一个性能问题。仅当列表包含某种带有“奇怪的复制构造函数/运算符”的特殊类型时,情况可能并非如此,但这可能表明设计不好。在极少数情况下,您还可能会遇到“隐式共享迭代器问题”,即在迭代器仍处于活动状态时分离(即深层复制)列表。 因此,最好的做法是通过编写以下内容来避免在所有情况下不需要的副本: QList<QString> q = ...; for (QString &x: qAsConst(q)) { .. }

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

    循环中的修改不会被破坏并按预期工作
  3. ,即它们的行为就像
  4. QList

    不使用隐式共享,而是在复制构造函数/运算符期间执行深层复制一样。例如, QList<QString> q; q.resize(10); QList<QString>& q2 = q; QList<QString> q3 = q; for (QString &x: q) {x = "TEST";}

    q
    q2

    相同,都包含10次“TEST”。

    q3
    是一个不同的列表,包含 10 个空 (null) 字符串。
    
    
    另请检查 Qt 文档本身有关 

    隐式共享
  5. 的信息,Qt 广泛使用它。在现代 C++ 中,这种性能优化构造可以(部分)被新引入的移动概念取代。

通过检查源代码提高理解 每个非常量函数在实际修改数据之前都会调用

detach

,例如。

[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
仅在列表实际共享时才有效地分离/深度复制数据
[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 个副本(= 除对象本身之外的另一个副本)。

	

© www.soinside.com 2019 - 2024. All rights reserved.