Does Range-Based For on Qt Container Do a Deep Copy?
-
Hi I scoured the web for this information but couldn't verify my suspicions... I am wondering if you do a simple ranged based for loop on a non-shared Qt container if the loop does a deep copy of the container. So like this:
QVector<QString> awesomeVector; awesomeVector << "awesome string" << "awesome string 2"; for (QString& string : awesomeVector) // does this do a deep copy? string = "unawesome string";
I'm betting my left arm there is no deep copy... hope I'm right or I'll be living the rest of my life without an arm. Thanks!
-
TLDR for googlers thx to Chris Kawa & JHilk:
QVector<QString> awesomeVector; for (QString& string : awesomeVector) // deep copy if container is shared & reference count greater than 1, otherwise no deep copy const QVector<QString> awesomeVector; for (const QString& string : awesomeVector) // no deep copy QVector<QString> awesomeVector; for (const QString& string : qAsConst(awesomeVector)) // no deep copy
-
You can read it yourself here
It's stated:
Note that using non-const operators and functions can cause QVector to do a deep copy of the data. This is due to implicit sharing.
QVector
is, like many other Qt classes, implicity shared -
@Crag_Hack said:
I'm betting my left arm there is no deep copy
The appropriate services will be collecting your left arm shortly :)
QVector<QString> awesomeVector; for (QString& string : awesomeVector) // copy const QVector<QString> awesomeVector; for (const QString& string : awesomeVector) // no copy QVector<QString> awesomeVector; for (const QString& string : qAsConst(awesomeVector)) // no copy
-
I'm actually on the OP's side of this, Q_FOREACH does the deep copy no matter what, the standard c++ for range loop is "usually" fine when working for references
as a proof:
class MyClass { public: QString str; // Default constructor MyClass(QString s) : str(std::move(s)) { qDebug() << "Default constructor called"; } // Copy constructor MyClass(const MyClass& other) : str(other.str) { qDebug() << "Copy constructor called"; } // Move constructor MyClass(MyClass&& other) noexcept : str(std::move(other.str)) { qDebug() << "Move constructor called"; } // Copy assignment operator MyClass& operator=(const MyClass& other) { str = other.str; qDebug() << "Copy assignment operator called"; return *this; } // Move assignment operator MyClass& operator=(MyClass&& other) noexcept { str = std::move(other.str); qDebug() << "Move assignment operator called"; return *this; } }; #include <QApplication> int main(int argc, char *argv[]) { QApplication app(argc,argv); QVector<MyClass> awesomeVector; awesomeVector << MyClass("awesome string") << MyClass("awesome string 2"); qDebug() << "Loop Start"; for (auto& myClass : awesomeVector) // does this do a deep copy? myClass.str = "unawesome string"; qDebug() << (awesomeVector.at(0).str) << awesomeVector.at(1).str; return app.exec(); }
results in no copy on write.
Default constructor called Move constructor called Default constructor called Move constructor called Loop Start "unawesome string" "unawesome string"
-
@J-Hilk Granted, this is a bit of a synthetic example, and since the vector is not actually shared at that point there is no copy.
But the copy happens if vector is shared. It happens in the non-const
begin()
member of the vector. If you run it under a debugger and break in thedetach()
method you'll see a callstack like this:QList<MyClass>::detach QList<MyCLass>::begin // <- this is called by the for loop main ...
The detach does copy if vector is shared at that point, so yeah, to be strict:
QVector<QString> awesomeVector; for (QString& string : awesomeVector) // this won't copy QVector<QString> awesomeVector; QVector<QString> sharedVector = awesomeVector; // this won't copy for (QString& string : awesomeVector) // but this will
so it won't copy, but it will detach, and detach might copy.
-
-
@J-Hilk yeah yeah, OP can keep the arm I guess. We'll get it next time :P
EDIT: an example came to mind:
void func(QVector<QString> awesomeVector) // this does shallow copy/sharing { for (QString& string : awesomeVector) // so this detach will always copy
so it's all about the context in which you run the for loop.
-
I think this all makes perfect sense since according to here it looks like the ranged based for loop uses iterators behind the scenes. And the non-const iterators won't trigger detach unless the container is shared. Which leads me to another question... will the following detach for a non-shared vector? Won't it just copy the string in the loop to the variable string each iteration but not actually do a deep copy of the container? Whereas if you do for (QString& string : awesomeVector) it just copies the variable string by reference.
QVector<QString> awesomeVector; //non-shared Qt container for (QString string : awesomeVector)
-
@Crag_Hack It's the same for reference and value cases. A range for loop is roughly equivalent to this:
for (auto it = awesomeVector.begin(); it != awesomeVector.end(); ++it) { QString string = *it; // in case of by value loop QString& string = *it; // in case of by reference loop ... }
The detach of the vector happens already in the call to begin(), so whether the value is later referenced or copied doesn't change the outcome. The only factor here is whether the vector is shared at that point or not.
That's why you should use qAsConst (or the std::as_const equivalent) when you only want to read from the vector. It basically const casts the vector, co calling
awesomeVector.begin()
usesconst
overload ofbegin
, which doesn't detach. -
TLDR for googlers thx to Chris Kawa & JHilk:
QVector<QString> awesomeVector; for (QString& string : awesomeVector) // deep copy if container is shared & reference count greater than 1, otherwise no deep copy const QVector<QString> awesomeVector; for (const QString& string : awesomeVector) // no deep copy QVector<QString> awesomeVector; for (const QString& string : qAsConst(awesomeVector)) // no deep copy
-