12

With old-style loops, I can delve into a QJsonArray and, in the example below, add element "foo" with contents of existing element "bar" for each array item. How can I do this using C++11 range-based for?

// QJsonArray oldArray contains an array of which one element is "bar"
QJsonArray newArray;
int i, b = oldArray.count();
for (i=0; i<n; ++i) {
    QJsonObject element = oldArray.at(i).toObject();
    element["foo"] = element["bar"];
    newArray.append(element);
}

I have tried the following (admittedly as trial and error):

auto&

for (auto& element : oldArray) {
    element["foo"] = element["bar];
    newArray.append(element);
}

I get the error

non-const lvalue reference to type 'QJsonValueRef' cannot bind to a temporary of type 'QJsonValueRef'

const auto&

for (const auto& element : oldArray) {
...

I get a warning

loop variable 'element' is always a copy because the range of type 'QJsonArray' does not return a reference

const auto

for (const auto element : oldArray) {
    element["foo"] = element["bar];
    ...

I get the error

no viable overloaded operator[] for type 'const QJsonValueRef'

relating to element["bar"]

Paul Masri-Stone
  • 2,843
  • 3
  • 29
  • 51

1 Answers1

12

The problem is that the iterator for QJsonArray returns a temporary QJsonValueRef object, and lvalue references can not bind to temporaries. We can either hold that temporary by value:

// QJsonArray oldArray contains an array of which one element is "bar"
QJsonArray newArray;
for (auto v : oldArray) {
    QJsonObject element = v.toObject();
    element["foo"] = element["bar"];
    newArray.append(element);
}

In this case v is a QJsonValueRef object (similar to what oldArray.at(i) gives in the old-style loop). After that, we convert that QJsonValueRef to a QJsonObject using .toObject().

Or we can use forwarding references since they can bind to rvalues:

for (auto&& v : oldArray) {
    ...
}

In this case v is deduced to be an rvalue reference to a QJsonValueRef.

Both solutions are identical in terms of the number of objects being created/destructed (since in the former, the copy is elided under C++17 guaranteed copy elision rules or even in pre-C++17 when using any decent compiler. In the latter, the reference is bound to the temporary and this prolongs its lifetime to match the whole iteration).


Notes

  1. This problem is similar to what happens when using range-based-for with a std::vector<bool>; since both std::vector<bool> and QJsonArrayhave iterators that return proxy objects.
  2. If using the Clang code model, the second solution generates the warning "loop variable 'v' is always a copy because the range of type 'QJsonArray' does not return a reference". See this question for an explanation.
Paul Masri-Stone
  • 2,843
  • 3
  • 29
  • 51
  • @user3606329 My starting point was “I want to use range-based for to iterate QJsonArray but was stuck with a load of errors.” I started asking the question - surely totally valid for StackOverflow - then worked out the answer and why, so thought I’d share it. – Paul Masri-Stone Apr 23 '18 at 11:32
  • @Mike I have edited the answer to use `auto&&`. If for no other reason, it avoids an unnecessary copy, making it a better solution. – Paul Masri-Stone Apr 23 '18 at 11:39
  • @PaulMasri-Stone, first of all it is not a copy, it is a move. Second, Your explanation is still a bit off. The iterator for `QJsonArray` returns an [rvalue `QJsonValueRef`](https://doc.qt.io/qt-5/qjsonarray-iterator.html#operator-2a) and you are binding that to `v`. Also, this statement is completely wrong "Because the rvalue is a QJsonValue, the lvalue v is now a QJsonValueRef."... – Mike Apr 23 '18 at 11:47
  • 1
    @Mike Would you like to edit the answer as I think you'll be more accurate than me? Totally happy for you to do that rather than me correcting it badly. – Paul Masri-Stone Apr 23 '18 at 11:49
  • @Mike I should have checked the `auto&& v` before changing the answer. I have now tried it and I get the warning "loop variable 'v' is always a copy because the range of type 'QJsonArray' does not return a reference". Can you help? – Paul Masri-Stone Apr 26 '18 at 04:13
  • What compiler / version / compiler-flags are you using to compile your program? I tried this code using various GCC, clang, MSVC versions and I couldn't reproduce the warning... – Mike Apr 26 '18 at 06:40
  • @Mike MacOS 10.13.3 (High Sierra), Qt Creator 4.6.0, Qt 5.10.1, Clang Code Model enabled – Paul Masri-Stone Apr 27 '18 at 08:59
  • Well, to silence the warning you need to fall back to `for(auto v : oldArray)` or use `//NOLINT`. I asked a [question](https://stackoverflow.com/q/50066139/2666212) about this warning (the question is talking about `vector` since more people are familiar with it, but the same logic applies here). – Mike Apr 27 '18 at 20:34
  • It turned out that the warning is just about making sure the code reflects the fact that there is a temporary object being returned; both solutions (`for auto v` and `for auto &&v`) are identical in terms of the number of objects being constructed/destructed, see the question for details. Sorry for the noise. Feel free to edit the answer however you like :) – Mike Apr 27 '18 at 20:34
  • @Mike Thanks for following up on this. I read that other question. Let's leave the solution as it is, but I'll add a note about the warning in case others encounter it. – Paul Masri-Stone Apr 30 '18 at 07:25
  • 1
    OK my turn to create lots of noise :-/ I've reread that other question and, if I've understood right, although the assignment to `auto&& v` doesn't involve a copy, a copy happens implicitly within the ranged-for operator. If that's the case, `auto v` is essentially equivalent to `auto&& v` but `auto v` explicitly expects to be receiving a copy. In which case `auto v` would be the better solution. You understand this better than me, so let me know if I'm wrong before I change the solution please. – Paul Masri-Stone Apr 30 '18 at 07:34
  • 1
    :-| You already did! D'oh. I've just added a note about the clang warning. Job done I hope. – Paul Masri-Stone Apr 30 '18 at 07:44