Smart PointersThose memory-managed objects are nice, but they're a bit of a pain to use on their own. Having to call AddRef()/Release() every time you deal with one isn't just tedious - it's asking for trouble. What would be good would be if AddRef and Release would just sort of.. call themselves, and that's where Smart Pointers come in. Smart Pointers are objects that behave (and, indeed, can be treated) just like pointers - except that they do more than plain variable pointers. In our case, we can set up a Smart Pointer class to call AddRef() on an object when it's assigned to it, and Release() when it lets go of it. Then we can just do 'ptr = obj' in code, and the smart pointer takes care of the reference counting for us! The faint-hearted amongst you: be warned that this next section uses most of C++'s 'advanced' features. If you're not comfortable with the *whole* language - including operator overloading and templates - leave now, and don't come back till you've bought several heavy books on the subject. Whether you beat yourself to death with them or actually read them is up to you. The rest of us: onward! Now. The first, most obvious thing to say, is that smart pointers will have a pointer to the object they're pointing at. (I said obvious, not easy). That is, a smart pointer object set to point at object 'cheese' will need to have a pointer member variable that actually points to 'cheese' - without it, we wouldn't get very far. The smart pointer class itself acts something like a wrapper for that pointer. But I ask you: what type should the pointer be? Veteran C programmers amongst you might suggest void*, but we can do better than that. The more astute of you may well say that IMMObject* would be suitable - it's better than void*, but they both suffer from the same problem, which is that I can mix my object types. I can take a pointer to an object of 'CMonkey,' and assign it to a pointer which something expects to have an object of type 'CTable.' (In short, they lack type safety). The best solution is to use templates, and have each smart pointer custom-built to store a particular type of object pointer. So here's the initial code: template<class T> class CMMPointer { protected: T* obj; public: //Constructors - basic CMMPointer() { obj=0; } //Constructing with a pointer CMMPointer(T *o) { obj=0; *this=o; } //Constructing with another smart pointer (copy constructor) CMMPointer(const CMMPointer<T> &p) { obj=0; *this=p; } //Destructor ~CMMPointer() { if(obj)obj->Release(); } //Assignement operators - assigning a plain pointer inline operator =(T *o) { if(obj)obj->Release(); obj=o; if(obj)obj->AddRef(); } //Assigning another smart pointer inline operator =(const CMMPointer<T> &p) { if(obj)obj->Release(); obj=p.obj; if(obj)obj->AddRef(); } }; OK. That will now let us create a smart pointer object, and assign to it an IMMObject* (the thing you assign to it has to be derived from something with AddRef()/Release() methods, at least, otherwise it won't compile). Still, it's pretty useless without some other basic pointer operations - like accessing the pointer. D'oh! Never mind. We can also take the opportunity to catch null pointer exceptions - our accessor functions can simply check that the pointer isn't NULL before returning it. Watch and learn: template<class T> class CMMPointer { protected: T* obj; public: //Constructors, destructor, and assignments are same as last time //Access as a reference inline T& operator *() const { assert(obj!=0 && "Tried to * on a NULL smart pointer"); return *obj; } //Access as a pointer inline T* operator ->() const { assert(obj!=0 && "Tried to -> on a NULL smart pointer"); return obj; } }; Almost there now. We're just missing a few more things - like, for example, a simple way to convert back to normal pointers, or a way to check whether the pointer is NULL without causing an assert() in the process :-) template<class T> class CMMPointer { protected: T* obj; public: //Constructors, destructor, assignments and accessors same as before //Conversion - allow the smart pointer to be automatically converted to type T* inline operator T*() const { return obj; } inline bool isValid() const { return (obj!=0); } inline bool operator !() { return !(obj); } inline bool operator ==(const CMMPointer<T> &p) const { return (obj==p.obj); } inline bool operator ==(const T* o) const { return (obj==o); } }; That should about do it.I've not included other operators - such as pointer math ops (+/-) - because it doesn't really make sense with smart pointers. You're meant to be pointing to objects, not arbitrary locations in memory. What we've got there, though, should be enough for 95% of the time - it should replace your average normal pointer absolutely transparently, with no need to change things - the only places where it's more complex is when converting one pointer type to another, and the aforementioned pointer math. There's ways of doing both. When should smart pointers be used? The simple answer is: any time you need to 'retain' a pointer - keep it for any length of time that might include a garbage collection sweep. You don't need to use smart pointers if you're just using the pointer in a single function and then dropping it from the stack. That can be particularly useful when deciding on parameters for functions: if SomeFunction(CMMPointer<SomeObject> &p) doesn't keep the pointer somewhere once it's returned, then it'd probably be easier to have it as SomeFunction(SomeObject *p). Accessing the object through a smart pointer obviously incurs a small speed cost, but it builds up; you should bear that in mind in speed-critical parts of the engine. Now that we have a smart pointer, it's time to create our very first memory-managed object - another part of the memory-manager system! :P Aside from actual game objects, the second most common dynamically allocated objects in our engine will be buffers. Buffers for decompressing resources, for serialising network messages.. you name it, there's a buffer for it. But you can't derive int[1000] from IMMObject - looks like we need another wrapper. Two, in fact - one for fixed-size buffers, and one for dynamic-sized (runtime-sized) buffers. The fixed-size one isn't really necessary, but it's *very* easy to do. These 'buffer wrappers' are the objects I affectionately term 'blobs,' and look like this: template<class T, int i> class CMMBlob : public IMMObject { protected: T buffer[i]; public: inline T& operator [](int index) { assert(index<i && "Bad index on CMMBlob::[]"); return buffer[index]; } inline operator T*() { return buffer; } AUTO_SIZE; }; template<class T> class CMMDynamicBlob : public IMMObject { protected: unsigned long dataSize; T *buffer; public: inline T& operator [](int index) { assert(index<dataSize && "Bad index on CMMDynamicBlob::[]"); return buffer[index]; } inline operator T*() { return buffer; } CMMDynamicBlob(unsigned long size) { dataSize=size; buffer=new T[size]; assert(buffer!=0 && "DynamicBlob buffer could not be created - out of memory?"); } ~CMMDynamicBlob() { if(buffer)delete[] buffer; } unsigned long size() { return dataSize+sizeof(this); } inline unsigned long blobSize() { return dataSize; } }; You see now why I said the fixed-size blob would be easy? That's just how easy simple objects are to handle - you can just group together a few variables in a class and have them memory-managed as a discrete object. The fixed-size blob takes the buffer type and size as template parameters; the dynamic blob takes the buffer type as the template parameter, and the buffer size as the constructor argument (so you can work it out at runtime). Note that the fixed-size blob uses the AUTO_SIZE macro, defined above, while the DynamicBlob reports the *actual* size of the object - including both the allocated buffer and the wrapper. If you just want the size of the buffer itself, you should use the seperate blobSize() function - because requiring you to remember to subtract the 8 bytes or so that the wrapper uses is, as usual, asking for problems. Each class provides access control, for two reasons: firstly, it's far too easy to severly mess things up by reallocating the buffer pointer or deleting it yourself (not that you'd ever want to do that, but accidents happen), so by forcing access to go through the [] and T* operators, we completely protect buffer itself from the outside world. It's still possible to do something like "delete (sometype*)(*obj);" but it's less likely because the syntax is more unweildy. The second reason is those asserts - we have the opportunity to check that we're not trying to access memory outside of the buffer. Woo! That, ladies and gentlemen, is the end of Memory Management. We now have a (relatively) robust system for tracking objects within our engine, and trust me, we'll be using it. It's totally independent of any other library or class (with the exceptions of the assert() calls), making it ideal for reuse. It doesn't depend on any platform-specific functionality, such as byte order. Looks like we're meeting the spec, then. On to... |
|