Table of contents
- Introduction
- Background
- Purpose of the SmartObject Class
- Pros and Cons of SmartObject Class
- SmartObject Class Implementation
- Constructor
- Copy Constructor
- Copy Operator (= operator)
- Destructor
- RetainObj
- ReleaseObj
- Debugging Code explained
- Use-case Examples
- Declaring a Class as a SmartObject Class
- Normal Use of SmartObject Class
- Reference Management Example
- Reference Management Example 2
- Reference Management Example 3
- More Practical Example Sources
- Conclusion
- Reference
Introduction
Memory Management is always a big issue in C++ development. There are many helpful classes for memory management such as Smart pointer, Auto pointer, etc. in C++. I believe many good ideas and practices had been presented in Code Project or elsewhere already. I just had a chance to spend sometime in Objective-C development, and I wondered how it would be if Objective-C style memory management is ported to C++. So this is the implementation of the Objective-C style memory management in C++. I am NOT saying this is the "Best Practice" for C++ development, but I thought it can be an interesting idea to share here.
Background
It is good to read the article, "How to create a Simple Lock Framework for C++ Synchronization," which is written by me, since the synchronization used for SmartObject class is from that article. You can also read about "Objective-C Memory Management," if you like to know about Objective-C Memory Management.
Purpose of the SmartObject Class
I must say, using SmartObject class, unlike smart-pointer or auto-pointers, can be very dangerous if not used in proper way. The purpose of SmartObject class was just my curiosity of porting Objective-C style memory management to C++. I sometimes use SmartObject class myself for memory management because I feel handy and simple, however that might be because I spent sometime in developing Objective-C projects, so the ease of use might not be true for your case.
Pros and Cons of SmartObject Class
- Pros
- Memory Management that is easy and simple to use for developers who have experience of Objective-C development (and for maybe others, too??)
- Easy to trace the reference holders in Debug mode. (if used in proper way)
- Cons
- Can be very dangerous if not used properly.
- Easy to get confused if not familiar with the idea of Objective-C memory management.
SmartObject Class Implementation
The SmartObject class is very simple as it only contains two methods "RetainObj," and "ReleaseObj" except the constructor/destructor/operator overload. So the structure of the SmartObject class will be as following:
- protected
SmartObject- Constructor and Copy-constructor
~SmartObject
- public
operator= - Copy operator overloading
RetainObj - Retain the object to hold
ReleaseObj
- Release the object holding
- private
- variables for reference counting, lock object, etc.
And also below is the skeleton declaration of
SmartObject class:
class SmartObject
{
public:
SmartObject & operator(const SmartObject&b);
void RetainObj();
void ReleaseObj();
protected:
SmartObject(); SmartObject(const SmartObject &b); virtual ~SmartObject(); private:
int m_refCount; };
- Note that the actual implementation of the
SmartObject is implemented at Header file due to Debugging purposes. Above class declaration is presented for ease of understanding. - Also note that synchronization code and debugging code is removed from code presented in this section for presentation purposes. Download the sources above to see full implementation.
Constructor
class SmartObject
{
...
protected:
...
SmartObject()
{
m_refCount=1;
}
...
};
On creation, it initialize the reference counter to 1. The reason is
- to make it able to match with the
ReleaseObj function like in Objective-C Memory Management. (alloc and release match) - to avoid the incorrect usage of
delete operator within Debug . - This will be explained in later section.
Also constructor is declared as
protected, since I don't want this class to be created by itself, and sub-classes should be able to access the constructor on its creation.
Copy Constructor
class SmartObject
{
...
protected:
...
SmartObject(const SmartObject&b)
{
m_refCount=1;
}
...
}; Same idea holds for copy constructor as constructor, but it is not copying anything from input SmartObject object because each object should have its own reference counter, and it should not be replaced with other object's reference count. If copy-constructor is not declared (which it will automatically use default copy-constructor), the reference counter will be replaced by the input SmartObject object's reference counter (which is something we don't want).
- Note that, it is not presented here, but for synchronization code, it actually copies the LockPolicy of input SmartObject object, and it creates its own lock according to the LockPolicy.
Copy Operator (= operator)
class SmartObject
{
...
public:
SmartObject &operator = (const SmartObject &b)
{
return *this;
}
...
};
- Note that, the reason that "
operator=" returns itself without doing anything is that I didn't want SmartObject object to be replaced by other object. If "operator=" is not implemented, when copy-operator is called, it will automatically call default copy-operator, and the values of reference counter will be replaced with other object's value. And the reason, I didn't make it private is that, if copy-operator is private, when some class A is a sub-class of SmartObject class, and when an object of class A tries to copy from other object, it will result an compiler-error.
Destructor
class SmartObject
{
...
protected:
...
~SmartObject()
{
m_refCount--;
assert(m_refCount==0);
}
...
}; The idea of SmartObject class (well, of course, it is the idea of Objective-C Memory Management) is that match new operator and ReleaseObj one to one correspondence, and match RetainObj and ReleaseObj function one to one correspondence. And I also wanted to use new and delete operator as normal C++ memory management, but to give some safety, I limited the usage of the delete operator only when the object's reference count is 1 by asserting if not the case.
So destructor decrement the reference counter by 1, and check if the reference count is 0 by assert. This will protect from the invalid use of delete operator as explained above. (since delete operator can be called only when the reference counter is 0 otherwise asserting.)
RetainObj
class SmartObject
{
public:
...
RetainObj()
{
m_refCount++;
}
...
};
RetainObj function is simple operation as shown above, it just increment the reference counter by 1, when it is called, so the idea is one RetainObj function should be matched with one ReleaseObj function.
ReleaseObj
class SmartObject
{
public:
...
Release()
{
m_refCount--;
if(m_refCount==0)
{
m_refCount++;
delete this;
return;
}
assert(m_refCount>=0);
}
...
};
For general case, it is simple, it just decrement the reference counter by 1. However when the reference counter reaches 0, the object must be deleted, since it means there is no other object referencing this object, so it is calling delete operator to delete itself.
- Note that the reason that this function increment the reference counter by 1 when the reference counter is 0, is that to support the
delete operator, as explained above section, since it is using same destructor whether it is from delete operator of ReleaseObj function or delete operator from elsewhere, to satisfy the requirement (which is delete operator must be called when the reference counter is 1), within ReleaseObj, it must make the reference counter to be 1 before calling delete operator.
Debugging Code explained
...
#define WIDEN2(x) L ## x
#define WIDEN(x) WIDEN2(x)
#define __WFILE__ WIDEN(__FILE__)
#define __WFUNCTION__ WIDEN(__FUNCTION__)
#if defined(_UNICODE) || defined(UNICODE)
#define __TFILE__ __WFILE__
#define __TFUNCTION__ __WFUNCTION__
#else#define __TFILE__ __FILE__
#define __TFUNCTION__ __FUNCTION__
#endif...
To make SmartObject class to be able to trace and manage the reference, it needs the reference holder's file name and function name. Since the compiler supports __FILE__ and __FUNCTION__ as a default functionality, I just declared __WFUNCTION__ and __WFILE__ by widening to support Unicode version. And to make it to support general purpose (both Unicode and non-Unicode version), I declared __TFILE__ and __TFUNCTION__ to be either __FILE__ and __FUNCTION__, or __WFILE__ and __WFUNCTION__ according to the UNICODE definition declaration.
class SmartObject
{
public:
...
void RetainObj(
#if defined(_DEBUG)
TCHAR *fileName, TCHAR *funcName, unsigned int lineNum
#endif )
{
...
#if defined(_DEBUG)
LOG_THIS_MSG(_T("%s::%s(%d) Retained Object : %d (Current Reference Count = %d)"),fileName,funcName,lineNum,this, this->m_refCount);
#endif }
void ReleaseObj(
#if defined(_DEBUG)
TCHAR *fileName, TCHAR *funcName, unsigned int lineNum
#endif )
{
...
#if defined(_DEBUG)
LOG_THIS_MSG(_T("%s::%s(%d) Released Object : %d (Current Reference Count = %d)"),fileName,funcName,lineNum,this, this->m_refCount);
#endif ...
}
...
protected:
SmartObject(
#if defined(_DEBUG)
TCHAR *fileName, TCHAR *funcName, unsigned int lineNum,
#endif )
{
...
#if defined(_DEBUG)
LOG_THIS_MSG(_T("%s::%s(%d) Allocated Object : %d (Current Reference Count = %d)"),fileName,funcName,lineNum,this, this->m_refCount);
#endif ...
}
SmartObject(
#if defined(_DEBUG)
TCHAR *fileName, TCHAR *funcName, unsigned int lineNum,
#endif const SmartObject& b)
{
...
#if defined(_DEBUG)
LOG_THIS_MSG(_T("%s::%s(%d) Allocated Object : %d (Current Reference Count = %d)"),fileName,funcName,lineNum,this, this->m_refCount);
#endif ...
}
...
};
#if defined(_DEBUG)
#define SmartObject(...) SmartObject(__TFILE__,__TFUNCTION__,__LINE__,__VA_ARGS__)
#define ReleaseObj() ReleaseObj(__TFILE__,__TFUNCTION__,__LINE__)
#define RetainObj() RetainObj(__TFILE__,__TFUNCTION__,__LINE__)
#endif
...
As it shown above, if it is in Debug mode, the parameters of RetainObj, ReleaseObj, SmartObject constructor, and SmartObject copy-constructor changed to as
(
#if defined(_DEBUG)
TCHAR *fileName, TCHAR *funcName, unsigned int lineNum,
#endif ...) This will allows above functions to receive the file name, function name and line number from the caller.
- Note that destructor is not traced, since it requires only catch the case in which invalid
delete operator is used, and the invalid delete operator usage will be caught by assertion within the destructor.
You can then trace the references by printing out the file name, function name, and line number of the caller of each function (RetainObj, ReleaseObj, SmartObjectconstructor, SmartObject copy-constructor) as below:
...
#if defined(_DEBUG)
LOG_THIS_MSG(_T("%s::%s(%d) Retained Object : %d (Current Reference Count = %d)"),fileName,funcName,lineNum,this, this->m_refCount);
#endif ...
#if defined(_DEBUG)
LOG_THIS_MSG(_T("%s::%s(%d) Released Object : %d (Current Reference Count = %d)"),fileName,funcName,lineNum,this, this->m_refCount);
#endif ...
#if defined(_DEBUG)
LOG_THIS_MSG(_T("%s::%s(%d) Allocated Object : %d (Current Reference Count = %d)"),fileName,funcName,lineNum,this, this->m_refCount);
#endif ...
If you download the source file, the default LOG_THIS_MSG is _tprintf. However, you can change this to whatever log system, you like according to your taste (for example OutputDebugString).
Also since it is very tiresome to input __TFILE__, __TFUNCTION__, __LINE__ for every call of RetainObj, ReleaseObj, etc. for Debug mode, by declaring below definitions, you can use the SmartObject as you do in Release mode, since it automatically inputs __TFILE__, __TFUNCTION__, and __LINE__ for you when it is in Debug mode.
...
#if defined(_DEBUG)
#define SmartObject(...) SmartObject(__TFILE__,__TFUNCTION__,__LINE__,__VA_ARGS__)
#define ReleaseObj() ReleaseObj(__TFILE__,__TFUNCTION__,__LINE__)
#define RetainObj() RetainObj(__TFILE__,__TFUNCTION__,__LINE__)
#endif...
- Note that this has some limitation of making unable to declare
RetainObj, ReleaseObj, SmartObject for any other purposes, where SmartObject class header is included, since they are declared as Preprocessor definition.
Use-case Examples
Declaring a Class as a SmartObject Class
#include "SmartObject.h"
class TestClass: public SmartObject
{
public:
TestClass(): SmartObject()
{
m_myVal=new int();
*m_myVal=1;
}
TestClass (const TestClass& b): SmartObject(b)
{
m_myVal = new int();
*m_myVal = *(b.m_myVal);
}
TestClass & operator=(const TestClass& b)
{
if(this!=&b)
{
*m_myVal=*(b.m_myVal);
SmartObject::operator=(b);
}
return *this;
}
virtual ~TestClass()
{
if(m_myVal)
delete m_myVal;
}
void DoSomething()
{
*m_myVal=*m_myVal+1;
}
private:
int *m_myVal;
}; You can make a class as a SmartObject class easily by sub-classing the SmartObject class as same as you subclass other classes.
Normal Use of SmartObject Class
...
TestClass testClass;
testClass.DoSomething();
...
To use the TestClass object, you can just instantiate the class with no problem and use as you always did in C++.
TestClass *testClass = new SomeClass(); ...
delete testClass ;
And also as above example, it can be used as the original C++ memory management, which is matching new and delete operator.
Reference Management Example
void SomeFunc(TestClass*sClass)
{
sClass->RetainObj(); ...
sClass->ReleaseObj(); }
...
void SomeOtherFunc()
{
TestClass *testObj= new TestClass (); SomeFunc(testObj);
testObj ->ReleaseObj(); } When passing the SmartClass object to a function (SomeFunc in this case), if receiving function retain the object by calling RetainObj, the reference count increases, and it should release the reference, when the use is done by calling ReleaseObj.
- Note that in above example, if
SomeFunc was a thread function, and if SomeFunc calls ReleaseObj function after SomeOtherFunc, "auto-release" will occur when SomeFunc calls ReleaseObj instead of ReleaseObj in SomeOtherFunc function.
Reference Management Example 2
SomeClass *someClass = new SomeClass(); someClass->RetainObj(); ...
someClass->ReleaseObj(); delete someClass;
SmartObject requires to match RetainObj and ReleaseObj in one to one correspondence. I generally recommend to match new operator and ReleaseObj as you match RetainObj and ReleaseObj, but it still allows you to match new and delete operator, and use RetainObj and ReleaseObj in between, with one limitation.
- To use
delete operator, the object's reference count MUST be 1. (reference count==1 means that there is no other reference object since creating the SmartObject takes one reference.)
Reference Management Example 3
void SomeFunc(SomeClass *sClass)
{
sClass->RetainObj(); ...
sClass->ReleaseObj(); }
...
TestClass *testClass = new TestClass(); testClass->RetainObj(); testClass->ReleaseObj(); SomeFunc(testClass);
testClass->ReleaseObj(); It doesn't matter how many times you retain the object by calling RetainObj function as long as you match with ReleaseObj function. And the object will be "auto-released," if ReleaseObj function is called when the reference count == 1.
More Practical Example Sources
More practical examples can be found from EpServerEngine Sources.
(Please, see "EpServerEngine - A lightweight Template Server-Client Framework using C++ and Windows Winsock" article for more detail.)
Conclusion
As I said in Introduction, this is NOT an explanation of "Best Practice of Memory Management in C++". But I believe it is something interesting to think about. For me, this SmartObject class ease my memory management when developing C++ project time to time. If someone feels easy and simple to use as I do, that will be great, and even if someone does not, I thought it might be an interesting idea for you to think about. Hope you enjoyed reading this article.
Reference
History
- 09.21.2012: - Table of Contents updated.
- 09.17.2012: - Submitted the article.