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) { .. }
- 是不是这样,除非容器被声明
const
,否则Qt会做一个
列表的副本然后遍历该副本?这不在
最好的,但如果列表很小(比如 10-20
QString 的).
- 这只是性能问题还是更深层次的问题
问题?假设我们没有 add/remove 个元素,而循环是 运行.
- 是循环中对值的修改(假设是
参考)仍然有效的东西或者它从根本上是
坏了?
Copy-on-write(=隐式共享)概念
重要的是要了解 copy-on-write (= implicit shared) 类 的外部行为与执行数据深拷贝的“正常”类 一样。他们只会尽可能长时间地推迟这种(可能)昂贵的复制操作。仅当出现以下序列时才会进行深层复制(=分离):
- 列表是隐式共享的,即对象按值复制(并且至少有 2 个实例仍然存在)
- 在隐式共享对象上访问了 non-const 成员函数。
您的问题
只有当容器被共享时(由这个列表的另一个写入实例复制),列表的一个副本将是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 成员函数。如果你自己写完整的,你应该使用 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) { .. }
确实复制了数据。
这主要是性能问题。仅当列表包含一些具有 奇怪副本 constructor/operator 的特殊类型时,情况可能并非如此,但这可能表明设计不当。在极少数情况下,您可能还会遇到 Implicit sharing iterator problem,方法是在迭代器仍处于活动状态时分离(即深拷贝)列表。
因此,在任何情况下避免不必要的副本都是一种很好的做法,方法是:
QList<QString> q = ...;
for (QString &x: qAsConst(q)) { .. }
或
const QList<QString> q = ...;
for (QString &x: q) { .. }
循环中的修改没有中断并按预期工作,即它们的行为就好像 QList
不使用隐式共享,但在复制 constructor/operator 期间执行深层复制。例如,
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 文档本身关于 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 个副本(= 除了对象本身之外的另一个副本)。
QList<QString> q;
for (QString &x: q) { .. }
- 是不是这样,除非容器被声明
const
,否则Qt会做一个 列表的副本然后遍历该副本?这不在 最好的,但如果列表很小(比如 10-20 QString 的). - 这只是性能问题还是更深层次的问题 问题?假设我们没有 add/remove 个元素,而循环是 运行.
- 是循环中对值的修改(假设是 参考)仍然有效的东西或者它从根本上是 坏了?
Copy-on-write(=隐式共享)概念
重要的是要了解 copy-on-write (= implicit shared) 类 的外部行为与执行数据深拷贝的“正常”类 一样。他们只会尽可能长时间地推迟这种(可能)昂贵的复制操作。仅当出现以下序列时才会进行深层复制(=分离):
- 列表是隐式共享的,即对象按值复制(并且至少有 2 个实例仍然存在)
- 在隐式共享对象上访问了 non-const 成员函数。
您的问题
只有当容器被共享时(由这个列表的另一个写入实例复制),列表的一个副本将是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 成员函数。如果你自己写完整的,你应该使用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) { .. }
确实复制了数据。
这主要是性能问题。仅当列表包含一些具有 奇怪副本 constructor/operator 的特殊类型时,情况可能并非如此,但这可能表明设计不当。在极少数情况下,您可能还会遇到 Implicit sharing iterator problem,方法是在迭代器仍处于活动状态时分离(即深拷贝)列表。
因此,在任何情况下避免不必要的副本都是一种很好的做法,方法是:
QList<QString> q = ...; for (QString &x: qAsConst(q)) { .. }
或
const QList<QString> q = ...; for (QString &x: q) { .. }
循环中的修改没有中断并按预期工作,即它们的行为就好像
QList
不使用隐式共享,但在复制 constructor/operator 期间执行深层复制。例如,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 文档本身关于 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 个副本(= 除了对象本身之外的另一个副本)。