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//defined(_UNICODE) || defined(UNICODE)
#define __TFILE__ __FILE__
#define __TFUNCTION__ __FUNCTION__
#endif//defined(_UNICODE) || defined(UNICODE)
...
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 //defined(_DEBUG)
)
{
...
#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 //defined(_DEBUG)
}
void ReleaseObj(
#if defined(_DEBUG)
TCHAR *fileName, TCHAR *funcName, unsigned int lineNum
#endif //defined(_DEBUG)
)
{
...
#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 //defined(_DEBUG)
...
}
...
protected:
SmartObject(
#if defined(_DEBUG)
TCHAR *fileName, TCHAR *funcName, unsigned int lineNum,
#endif //defined(_DEBUG)
)
{
...
#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 //defined(_DEBUG)
...
}
SmartObject(
#if defined(_DEBUG)
TCHAR *fileName, TCHAR *funcName, unsigned int lineNum,
#endif //defined(_DEBUG)
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 //defined(_DEBUG)
...
}
...
};
#if defined(_DEBUG)
#define SmartObject(...) SmartObject(__TFILE__,__TFUNCTION__,__LINE__,__VA_ARGS__)
#define ReleaseObj() ReleaseObj(__TFILE__,__TFUNCTION__,__LINE__)
#define RetainObj() RetainObj(__TFILE__,__TFUNCTION__,__LINE__)
#endif//defined(_DEBUG)
...
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 //defined(_DEBUG)
...)
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
, SmartObject
constructor, 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 //defined(_DEBUG)
...
#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 //defined(_DEBUG)
...
#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 //defined(_DEBUG)
...
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//defined(_DEBUG)
...
- 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
- 08.22.2013: - Re-distributed under MIT License
- 09.21.2012: - Table of Contents updated.
- 09.17.2012: - Submitted the article.