Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VC10.0

SmartObject Class (Objective-C like Memory Management) for C++ (A substitute for Smart Pointer maybe?)

4.55/5 (8 votes)
22 Aug 2013MIT9 min read 36.3K   351  
This article explains Objective-C like C++ memory management class, SmartObject.

Table of contents 

  1. Introduction
  2. Background
  3. Purpose of the SmartObject Class
  4. Pros and Cons of SmartObject Class
  5. SmartObject Class Implementation 
    1. Constructor
    2. Copy Constructor
    3. Copy Operator (= operator)
    4. Destructor
    5. RetainObj
    6. ReleaseObj
  6. Debugging Code explained
  7. Use-case Examples
    1. Declaring a Class as a SmartObject Class
    2. Normal Use of SmartObject Class
    3. Reference Management Example
    4. Reference Management Example 2
    5. Reference Management Example 3
  8. More Practical Example Sources
  9. Conclusion
  10. 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
      • Destructor 
  • 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:
C++
// SmartObject.h
 
class SmartObject
{
public:
   SmartObject & operator(const SmartObject&b);
   void RetainObj();
   void ReleaseObj();
protected:
   SmartObject();                      // constructor 
   SmartObject(const SmartObject &b);  // copy constructor 
   virtual ~SmartObject();             // destructor 
private: 
   int m_refCount;             // reference counter  
};  
  • 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

C++
// SmartObject.h
 
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

C++
// SmartObject.h
 
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)

C++
// SmartObject.h
 
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

C++
// SmartObject.h
 
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

C++
// SmartObject.h
 
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

C++
// SmartObject.h
 
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

C++
// Defines.h
 
...
#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. 

C++
// SmartObject.h
 
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 

C++
( 
#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, SmartObjectconstructor,  SmartObject copy-constructor) as below:  

C++
... 
#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. 

C++
... 
#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

C++
// TestClass.h
 
#include "SmartObject.h" 
class TestClass: public SmartObject
{ 
public:
   TestClass():  SmartObject()
 
   {
      //Initialization 
      m_myVal=new int();
      *m_myVal=1; 
   } 
   TestClass  (const TestClass& b): SmartObject(b)
   {
      //Initialization by copying
      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

C++
... 
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++. 

C++
TestClass *testClass = new SomeClass(); // reference count = 1
...
delete testClass ;                   // deletion (Only allowed when reference count is 1)      

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

C++
void SomeFunc(TestClass*sClass)
{ 
   sClass->RetainObj();  // reference count =  2
   ...
   sClass->ReleaseObj(); // reference count =  1
}
... 
void SomeOtherFunc()
{ 
   TestClass *testObj= new TestClass (); // reference count = 1
   SomeFunc(testObj);
   testObj ->ReleaseObj();               // reference count = 0, and auto-released
}    

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

C++
SomeClass *someClass = new SomeClass(); // reference count = 1
someClass->RetainObj();                 // reference count = 2
...
someClass->ReleaseObj();                // reference count = 1
delete someClass;                       // deletion (Only allowed when reference count is 1) 

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

C++
void SomeFunc(SomeClass *sClass)
{
   sClass->RetainObj();  // reference count = 2 
   ... 
   sClass->ReleaseObj(); // reference count = 1 
} 
... 
TestClass *testClass = new TestClass(); // reference count = 1
testClass->RetainObj();                 // reference count = 2 
testClass->ReleaseObj();                // reference count = 1
SomeFunc(testClass); 
testClass->ReleaseObj();                // reference count = 0, and auto-release   

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. 

License

This article, along with any associated source code and files, is licensed under The MIT License