19

In Qt 5.2.1, how is it that the following code results are different?

QVector<int> c;
if (c.cbegin() != c.begin())
{
   std::cout << "Argh!" << std::endl;
}

this prints "argh" but the following doesn't.

QVector<int> c;
if (c.begin() != c.cbegin())
{
    std::cout << "Argh!" << std::endl;
}

Notice that cbegin and begin places are switched. But if you change the container state I mean for example push_back something in it, it works correctly. Seems to me that before calling any mutable method on container, cbegin and cend are invalid. Is it a bug or feature?

R.J
  • 396
  • 1
  • 13
  • 4
    Um, there's always `std::vector`. `;)` – Mark Garcia Aug 27 '14 at 06:08
  • 3
    How can this be a *feature*? – Praetorian Aug 27 '14 at 06:12
  • Mark, tell that to our DA CTO :) – R.J Aug 27 '14 at 06:13
  • I don't know. It might have something to do with write on demand feature of Qt containers. I checked all Qt containers and same behavior. – R.J Aug 27 '14 at 06:14
  • Not sure if this makes a difference, but the documentation says the iterators are just typedefs for pointers. –  Aug 27 '14 at 06:20
  • 2
    I traced into begin and cbegin. cbegin method just return the pointer to internal buffer and begin method first call the detach method (write on demand). Which in this case (default constructed) detach calls reallocData which in turn change the internal buffer pointer and begin returns this new pointer. By this I think it's a bug and should be reported. – R.J Aug 27 '14 at 06:33
  • Well, it's not bug, but I still would be hesitant to call it a feature :) – Praetorian Aug 27 '14 at 06:50
  • This is *undefined behaviour*. Const and non-const iterators are not comparable. Undefined behaviour means it may do whatever. So it just *seems* to be working in one case and not workign in another while in fact it's wrong in both cases. – GreenScape Aug 27 '14 at 07:13
  • 3
    @GreenScape No, as far as standard library containers are concerned, the [comparison is well defined](http://stackoverflow.com/questions/16900498/const-to-non-const-iterator-comparisons-are-they-valid). I don't know much about Qt, but given that they document *STL-style iterators* are typedefs for plain pointers, the comparison itself is well defined as far as the language is concerned. The result *should* be as expected too, but the fact that it fails is an unfortunate implementation detail. – Praetorian Aug 27 '14 at 07:21
  • @Praetorian what makes you think `QVector` is a *standard library container*? – GreenScape Aug 27 '14 at 07:25
  • Yep I though that Qt containers follow the standard library rules at least for STL like iterators and functions so that it is possible to simply exchange std::vector with QVector and vice versa. But it seems there are some corner cases doing that. – R.J Aug 27 '14 at 07:27
  • @R.J, @Praetorian it seems they do. *STL* states that `iterator` can be implicitly upgraded to `const iterator`. That's why first case works, ie `QVector::const_iterator::operator=(const QVector::const_iterator &)`. But not `QVector::iterator::operator=(const QVector::iterator &)` as in second case. That is `const iterator` can not be downgraded to `iterator`. So behaviour should be *undefined*. I am surprised it compiles though. – GreenScape Aug 27 '14 at 07:32
  • 2
    @GreenScape No, the standard library containers guarantee that "in the expressions `i == j` (etc.) where `i` and `j` denote objects of a container's `iterator` type, either or both may be replaced by an object of the container's `const_iterator` type referring to the same element with no change in semantics". – T.C. Aug 27 '14 at 08:12
  • @GreenScape There is no `QVector::const_iterator::operator=` because `QVector::const_iterator` is `T const *` and `QVector::iterator` is `T *`. If you compare the two for equality the non-`const` one will be implicitly `const` qualified, and the comparison compiles. – Praetorian Aug 27 '14 at 16:26

2 Answers2

22

The behavior you're observing has to do with the order of the calls being made to QVector::begin and QVector::cbegin. If the call to the former happens before the call to the latter, then their return values compare equal, but if you reverse the order, they do not compare equal. This can be demonstrated by the following code:

QVector<int> c;
std::cout << static_cast<void const *>(c.begin()) << std::endl;
std::cout << static_cast<void const *>(c.cbegin()) << std::endl;

The addresses printed will be the same. However, if you swap the order of the two print statements, the addresses will be different.

If you dig into the source code for the two member functions, you'll see

inline iterator begin() { detach(); return d->begin(); }
inline const_iterator cbegin() const { return d->constBegin(); }

And tracing back further, the bodies of both d->begin() and d->constBegin() are identical. So the difference is in the call to QVector::detach() in the first case. This is defined as

template <typename T>
void QVector<T>::detach()
{
    if (!isDetached()) {
#if QT_SUPPORTS(UNSHARABLE_CONTAINERS)
        if (!d->alloc)
            d = Data::unsharableEmpty();
        else
#endif
            reallocData(d->size, int(d->alloc));
    }
    Q_ASSERT(isDetached());
}

An explanation of what's going on can be found here.

Qt’s containers are implicitly shared – when you copy an object, only a pointer to the data is copied. When the object is modified, it first creates a deep copy of the data so that it does not affect the other objects. The process of creating a deep copy of the day is called detach

Since, STL-style iterators are conceptually just pointers, they make modification to the underlying data directly. As a consequence, non-const iterators detach when created using Container::begin() so that other implicitly shared instances do not get affected by the changes.

So, if the call to QVector::begin() happens first, then the container's underlying storage is reallocated, and the subsequent call to QVector::cbegin() will return the same pointer. However, reverse them and the call to QVector::cbegin() returns a pointer to the shared storage before any reallocation happens.

Praetorian
  • 106,671
  • 19
  • 240
  • 328
  • Hmm... but objects start off detached. Why would the example you gave result in different pointers depending on the order you create the iterators? – Vitali Aug 30 '14 at 03:55
  • @Vitali It seems objects do not start off detached, which is very strange. If I add `std::cout << c.isDetached() << std::endl;` before calling either `begin()/cbegin()` it prints `0`. – Praetorian Aug 30 '14 at 22:16
  • I could be wrong but then the original post is in undefined behaviour land. The evaluation order of the two functions is undefined. It happens to work the way it does on his compiler & machine combination but it won't necessarily anywhere else. I found the spot in the Qt source code & understand why it's shared now. It seems like a silly design. The default construction uses a sentinel object that is static & shared. – Vitali Apr 10 '15 at 00:19
  • It definitely seems like a bug: Q_REFCOUNT_INITIALIZE_STATIC is what the ref count for the shared QArrayData representing null should be using. – Vitali Apr 10 '15 at 00:23
  • Never mind, it's not a bug. I filed QTBUG-45490 but then I realized the behaviour is correct. Consider the case where you were to try to insert elements using this iterator - if it wasn't detached then you'd be inserting into a shared data structure. The only way to solve that would be to add if(d) checks throughout to handle the default case which likely hurts performance in the normal case which is why it's designed this way. – Vitali Apr 10 '15 at 00:41
  • @Vitali There's no undefined behavior as far as the language is concerned. The order of evaluation of the two functions is *unspecified* so under different conditions the OPs results might be reversed or not reproducible at all, but the code doesn't exhibit undefined behavior. I understand the need to detach the container on the call to the non-`const` `begin()` overload, but what I don't get is the decision to make a container not detached upon initial construction. I'd have chosen to make it not detached only once a copy happens. – Praetorian Apr 10 '15 at 02:44
9

The test code you used is very similar to a bug report which was filed in 2012. It was closed as invalid, because

constBegin and begin should not be compared. Ever. This is not correct usage at all (and can be caught, with strict iterator checks), so there's nothing to fix here.

Which is true. But the function begin() is overloaded as

 QVector<T>::iterator       QVector::begin();
 QVector<T>::const_iterator QVector::begin() const;

This is an unspecified bevahior, as the order of evaluation of the operands of the C++ == operator is unspecified. There is no concept of left-to-right or right-to-left evaluation in C++.

So depending on the compiler and optimizations, you will end up with either the iterator version of begin or the const_iterator version.

UmNyobe
  • 22,539
  • 9
  • 61
  • 90
  • +1 for finding the bug report. Having the two overloads of `begin()` is not really a problem under normal usage because you'd have to cast one of the operands to get the two different overloads to be called in the same comparison. And if you did that, the result would be unspecified, not undefined, same as the order of evaluation of the operands. – Praetorian Aug 27 '14 at 07:18
  • 1
    Wait a sec, where is the UB here? – T.C. Aug 27 '14 at 07:35
  • @T.C. unspecified, fixed. – UmNyobe Aug 27 '14 at 07:40
  • What determines if you'll get the `iterator` or `const_iterator` version is the object it's being called on. If it's `const`, then you get the `const_iterator` and otherwise you get the `iterator`. – Dave Johansen Sep 08 '14 at 20:53