11.5.  Flyweight Pattern: Implicitly Shared Classes

[ fromfile: refcount.xml id: refcount ]

Unlike Java, C++ has no garbage collection. Garbage collection is a thread that recovers heap memory that is no longer referenced. It runs when the CPU is relatively idle, or when it is running out of memory. When an object is no longer referenced, it is deleted, and the memory that it occupied is made available for use by other objects. It has the benefit of being less work for the developer, who does no need to worry about memory leaks [86] but is is certainly more work for the CPU.

The next examples show a way of building garbage collection into the design of a class by means of reference counting. Reference counting is an example of resource sharing. It is considered much more efficient, in terms of both developer and CPU time, than depending on a garbage collector to manage the heap.

Each object keeps track of its active references. When an object is created, its reference counter is set to 1. Each time the object is newly referenced, the reference counter is incremented. Each time it loses a reference, the reference counter is decremented. When the reference count becomes 0, the shared object can be deallocated.

[Note] What About Changes?

If the object is about to be changed (e.g., a non-const member function is called), and its reference count is greater than 1, it must be detached first so that it is no longer shared.

Implicitly shared classes work by reference-counting, to prevent the accidental deletion of shared managed objects. Clients using this class need not be concerned with reference counts or memory pointers.

QString, QVariant, and QStringList are all implemented this way, meaning that it is fast to pass and return these by value. If you need to change objects from inside a function, you should pass by reference, rather than by pointer.

It is still slightly faster to pass by const reference, which enables C++ to optimize out the copy operation entirely. With const reference, the function cannot make changes to the reference, and automatic conversions do not happen.

To make your own implicitly shared flyweight, you can write your own reference counting code, or reuse two Qt classes: QSharedData and QSharedDataPointer.

QSharedData provides a public QAtomicInt ref member, for a reference count. QAtomicInt provides a deref() operation, which is used by QSharedDataPointer to decrement and test, to determine if it can safely delete the shared data. QSharedDataPointer updates the reference count of its shared data depending on whether it is being copied or detached.

Figure 11.4.  Example QSharedData Private Implementation

Example QSharedData Private Implementation

This example starts with a relatively mundane, non-sharing MyString class that implements strings with dynamic arrays of char, shown in Example 11.15.

Example 11.15. src/mystring/shareddata/mystring.h

#ifndef MYSTRING_H
#define MYSTRING_H
#include <iostream>
class MyString {
public:
    MyString(const MyString& str); 
    MyString& operator=(const MyString& a);
    MyString(); 
    MyString(const char* chptr);  
    explicit MyString(int size);
    virtual ~MyString();
    friend std::ostream& operator<<(std::ostream& os, const MyString& s);
    int length() const;
    MyString& operator+= (const MyString& other);
    friend MyString operator+(const MyString&, const MyString&);
protected:
    int   m_Len;
    char* m_Buffer;    1
    void  copy(const char* chptr);
};
#endif        //  #ifndef MYSTRING_H

1

Pointer to the start of dynamic array

<include src="src/mystring/shareddata/mystring.h" href="src/mystring/shareddata/mystring.h" id="mystring-h" allfiles="true" mode="cpp"/>


Example 11.16 extends the MyString class by adding reference counting capability. This is the private implementation class.

Example 11.16. src/mystring/shareddata/stringdata.h

[ . . . . ]
class StringData : public QSharedData, public MyString {
public:
    friend class SString;
    StringData() {}
    StringData(const char* d) : MyString(d) {}
    explicit StringData(int len) : MyString(len) {}
    StringData(const StringData& other) 
             : QSharedData(other), MyString(other) {}    
};
[ . . . . ]

<include src="src/mystring/shareddata/stringdata.h" href="src/mystring/shareddata/stringdata.h" id="stringdata-h" allfiles="true" mode="cpp"/>


The implicitly shared SString, shown in Example 11.17, is an example of a class that uses the QSharedDataPointer to achieve copy on write.

Example 11.17. src/mystring/shareddata/sstring.h

[ . . . . ]
class SString {
public:
    SString();
    explicit SString(int len);
    SString(const char* ptr);
    SString& operator+= (const SString& other);
    int length() const;
    int refcount() const {return m_d->ref;}
    friend SString operator+(SString, SString);
    friend std::ostream& operator<< (std::ostream&, const SString&);
[ . . . . ]
private:
    // Private Implementation Pattern
    QSharedDataPointer<StringData> m_d;
    
};
[ . . . . ]

<include src="src/mystring/shareddata/sstring.h" href="src/mystring/shareddata/sstring.h" id="sstring-h" mode="cpp"/>


The public methods of SString delegate to StringData. The actual shared data is copied automatically whenever m_d is dereferenced in a non-const way. Example 11.18 demonstrates that the refcount() decreases when one of the shared flyweights is modified.

Example 11.18. src/mystring/shareddata/sharedmain.cpp

#include <iostream>
#include "sstring.h"
using namespace std;

void passByValue(SString v) {
    cout << v << v.refcount() << endl; 1
    v = "somethingelse";
    cout << v << v.refcount() << endl; 2
}

int main (int argc, char* argv[]) {
    SString s = "Hello";
    passByValue(s);
    cout << s << s.refcount() << endl; 3
}

1

refcount=2

2

refcount=1

3

refcount=1

<include src="src/mystring/shareddata/sharedmain.cpp" href="src/mystring/shareddata/sharedmain.cpp" id="sharedmain-cpp" mode="cpp"/>


QExplicitlySharedDataPointer is the same as QSharedDataPointer, but you must explicitly call detach() on the QSharedData each time that a copy is needed.

Most Qt classes implement the Flyweight Pattern, for either implicit sharing, or for other reasons. Only when the copy is actually modified are the collected objects cloned and detached from the original container. That is when there will be a time/memory penalty.



[86] Actually, Java developers do worry and often try many tricks to create fewer heap objects. They also have ways to force the garbage collector to run more frequently (and, of course, consume more CPU cycles).