Search code examples
c++qtqlistcopy-on-write

Qt undocumented method setSharable


I stumbled about a method which seems to be present in all data objects like QList, QQueue, QHash...

I even investigated so far I can see the source code of it, which is

inline void setSharable(bool sharable) {
    if (!sharable) detach(); d->sharable = sharable;
}

in qlist.h (lines 117).

But what effect does it have on the QList, QQueue, QHash... ? And is it in any way related to threading (which sounds reasonable)?

Thanks for any answer, and please only answer if you got actual knowledge.


Solution

  • The sharable state you're asking about has nothing to do with mutlithreading. It is instead an implementation detail of copy-on-write data classes (even single-threaded ones) that hand out references to internal state.

    Consider a class String that is implemented using CoW (for illustration purposes, this class isn't usable in threaded contexts, because accesses to d->refcount aren't synchronised, it also doesn't ensure that the internal char arrary ends in '\0', and might as well eat your grandmother; you have been warned):

    struct StringRep {
        StringRep()
            : capacity(0), size(0), refcount(0), sharable(true), data(0) {}
        ~StringRep() { delete[] data; }
        size_t capacity, size, refcount;
        bool sharable; // later...
        char * data;
    };
    
    class String {
        StringRep * d;
    public:
        String() : d(new StringRep) { ++d->refcount; }
        ~String() { if (--d->refcount <= 0) delete d; }
        explicit String(const char * s)
            : d(new StringRep)
        {
            ++d->refcount;
            d->size = d->capacity = strlen(s);
            d->data = new char[d->size];
            memcpy(d->data, s, d->size);
        }
        String(const String &other)
            : d(other.d)
        {
            ++d->refcount;
        }
        void swap(String &other) { std::swap(d, other.d); }
        String &operator=(const String &other) {
            String(other).swap(*this); // copy-swap trick
            return *this;
        }
    

    And a sample function each for mutating and const methods:

        void detach() {
            if (d->refcount == 1)
                return;
            StringRep * newRep = new StringRep(*d);
            ++newRep->refcount;
            newRep->data = new char[d->size];
            memcpy(newRep->data, d->data, d->size);
            --d->refcount;
            d = newRep;
        }
    
        void resize(size_t newSize) {
            if (newSize == d->size)
                return;
            detach(); // mutator methods need to detach
            if (newSize < d->size) {
                d->size = newSize;
            } else if (newSize > d->size) {
               char * newData = new char[newSize];
               memcpy(newData, d->data, d->size);
               delete[] d->data;
               d->data = newData;
            }
        }
    
        char operator[](size_t idx) const {
            // no detach() here, we're in a const method
            return d->data[idx];
        }
    
    };
    

    So far so good. But what if we want to provide a mutable operator[]?

        char & operator[](size_t idx) {
            detach(); // make sure we're not changing all the copies
                      // in case the returned reference is written to
            return d->data[idx];
        }
    

    This naïve implementation has a flaw. Consider the following scenario:

        String s1("Hello World!");
        char & W = s1[7]; // hold reference to the W
        assert( W == 'W' );
        const String s1(s2); // Shallow copy, but s1, s2 should now
                             // act independently
        W = 'w'; // modify s1 _only_ (or so we think)
        assert( W == 'w' ); // ok
        assert( s1[7] == 'w' ); // ok
        assert( s2[7] == 'W' ); // boom! s2[7] == 'w' instead!
    

    To prevent this, String has to mark itself non-sharable when it hands out a reference to internal data, so that any copy that is taken from it is always deep. So, we need to adjust detach() and char & operator[] like this:

        void detach() {
            if (d->refcount == 1 && /*new*/ d->sharable)
                return;
            // rest as above
        }
        char & operator[](size_t idx) {
            detach();
            d->shareable = false; // new
            return d->data[idx];
        }
    

    When to reset the shareable state back to true again? A common technique is to say that references to internal state are invalidated when calling a non-const method, so that's where shareable is reset back to true. Since every non-const function calls detach(), we can reset shareable there, so that detach() finally becomes:

        void detach() {
            if (d->refcount == 1 && d->sharable) {
                d->sharable = true; // new
                return;
            }
            d->sharable = true; // new
            StringRep * newRep = new StringRep(*d);
            ++newRep->refcount;
            newRep->data = new char[d->size+1];
            memcpy(newRep->data, d->data, d->size+1);
            --d->refcount;
            d = newRep;
        }