Introduction
During the run, programs create/allocate different kinds of objects/resources. And, most of them require some sort of cleanup. For example, if you allocate a memory/object via the new
operator, then you have to delete
it later; opening a file via CreateFile
requires you to CloseHandle
; sockets require closesocket
, GDI objects require DeleteObject
, and so on. Hi-level languages do this automatically, whereas C/C++ programmers have to do this themselves, this is the price of flexibility.
Once some function return, you a pointer/handle to a valid object of some kind - you must destroy it later by appropriate means. That is, you have to track the lifetime of such an object. Any mistake - oops, we have a memory/resource leak. Sometimes, you need to create an object for a short duration, say for some function. Then, you need to free it before the function returns. When such a function has multiple return
statements - the code becomes ugly: either you put the cleanup statement before every return
, or rewrite the function to have a single return
statement. You can also use a SEH mechanism: put all the cleanups in the __finally
block. In fact, that is what you must do if you work with exception handling: upon exception, you'll not reach the return
statement.
If you do the work in plain C, then you seem to have no choice, but there's a more elegant way in C++. One of the most valuable and important features of C++ is destructors. That is, the compiler is responsible to track the lifetime of C++ objects and call their destructors when necessary. If you declare an object in a scope/function, then its lifetime is limited to this scope/function, and C++ guarantees that upon leaving it, the destructor will be called. All you have to do is place the pointer/handle inside some object that you declare in the scope, and free it in the destructor. Make the compiler work instead of you.
Well, I'm not the first who thought about this, of course; this is a well-known technique. I've been using and enjoying it for a long time. But recently, I've discovered an even more elegant way to use it.
Suppose we want to create a class that wraps BSTR
s. The cleanup function is SysFreeString
. So, we declare a class, say Bstr_G
(the G suffix means 'guarded'), for it. Now, what do we want to have in this class ?
- Empty constructor, that sets the encapsulated
BSTR
to NULL
. - Constructor that receives the
BSTR
and saves it. - Destructor (most important) that destroys the
BSTR
if it is non-NULL
. - A cast to type
BSTR
. - Explicit
Destroy
. If the BSTR
is non-NULL
- free it and set to NULL
. Attach
. Destroy the previously held BSTR
(if valid) and save the new one.Detach
. Return the BSTR
and set it to NULL
without destroying it.- Assignment operator, similar to
Attach
. private
/protected
copy constructor and assignment operator. There should not be more than one object that wraps the same BSTR
, hence it is good to disable them.
Not too complex, a bit of routine work. Suppose, now, that you need a class that wraps an HICON
. You declare a class, say HIcon_G
, that has similar functionality, and write all the things again, except you put HICON
instead of BSTR
, and DestroyIcon
instead of SysFreeString
. In such a way, you have to do a lot of routine work to prepare such classes for different types.
Rewriting the same thing multiple times is not a good practice. It's very annoying, you can make a mistake during the rewrite. Plus, if you find a bug eventually (or want to add some extra functionality), you have to fix it in all the classes. Hence, I decided to use a more elegant way: template classes. That's exactly what they are for.
That is, instead of writing the guard classes for different types, we'll write a single template class that takes the following parameters:
- The type we're encapsulating.
- The 'invalid' value for this type (usually
NULL
). - The cleanup function.
Then, just instantiate the specialized template version for all the types you want.
Implementation
There's no need to explain the code in-depth. It's not too complex, you can figure it out by yourself. Let me just point out some important things:
There's a base class GBase_T
, which shouldn't be used as is. Instead - there're two classes inherited from it: GObj_T
and GRef_T
. The GObj_T
class encapsulates simple types like those in our example, whereas GRef_T
encapsulates types that support referencing. In particular, when you save some value, it demands to call an additional function; for such types, copy constructors can be enabled, the meaning of assignment and attaching is different, and etc. It can be used to implement smart pointers or something similar.
Next, you can pass a type and a value as a template parameter, but you can't pass a function (that is needed for cleanup). Hence, in order to use those template classes, you should pass another class that has the needed function declared, that's how it's implemented. That class that you pass must provide the following things:
- A
typedef
statement that defines the GuardType
. - The value itself saved in the
m_Value
member. - An invalid value for this type, through a static function
GetNullValue
. - Optionally, a reference method, which is called if you use it with
GRef_T
.
Providing such a class can also be simplified in most of the cases. There're some different template base classes for this purpose (GBaseH_XXX
).
Usage example
Let's give a couple of examples about how to use it exactly.
For example, you want an HICON
guard that is created via CreateIcon
(don't use it for icons loaded via LoadIcon
, they don't need cleanup).
struct GBaseH_DestroyIcon : public GBaseH_CoreNull<HICON>
{
void Destroy() { VERIFY(DestroyIcon(m_Value)); }
};
typedef GObj_T<GBaseH_DestroyIcon> HIcon_G;
HIcon_G hIcon = CreateIcon( ... );
if (hIcon)
{
return;
}
if ( ... )
return;
return;
File handle encapsulation example is shown next. Pay attention: the 'invalid' value for file handles is not NULL
, it's INVALID_HANDLE_VALUE
. Then, you may declare it this way:
struct GBaseH_FileClose : public GBaseH_Core<HANDLE>
{
static HANDLE GetNullValue() { return INVALID_HANDLE_VALUE; }
void Destroy() { VERIFY(CloseHandle(m_Value)); }
};
typedef GObj_T<GBaseH_FileClose> HFile_G;
HFile_G hFile = CreateFile( ... );
if (hFile)
{
WriteFile(hFile, ... );
}
Attention: When you use the hFile
in the expression that expects a HANDLE
(WriteFile
) - our object returns the wrapped handle, but when you use hFile
in a conditional statement (if
) - then our object returns if it differs from INVALID_HANDLE_VALUE
. For types in which invalid value equals to 0/NULL
, this is the same, but in our case, this is different. If you're not certain about some expression and afraid of the mess - you can use IsValid
and GetValue
explicitly.
As an example of reference-able objects, we may demonstrate smart pointers:
struct GBaseH_IMyInterface : public GBaseH_CoreNull<IMyInterface>
{
void Reference()
{
m_Value->AddRef();
}
void Destroy()
{
m_Value->Release();
}
};
typedef GRef_T<GBaseH_IMyInterface> IMyInterfacePtr;
IMyInterfacePtr pMyObj = ... ;
IMyInterfacePtr pMyObj2 = pMyObj;
Additional notes
If you tend to use exception handling (as I do) - you'll probably find this method very handy. Because otherwise, you'd have to place a __try
- __finally
block in every function that needs a cleanup.
You should, however, know that the compiler supports two exception handling models: the so-called synchronous and asynchronous. In the first model, the compiler assumes that exceptions may not occur unless you put a throw
statement or call another function that may use throw
, whereas the second model states that exceptions may occur everywhere. In the first case, you get a bit smaller and faster code (sometimes the compiler skips __try
- __finally
blocks), whereas in the second case, you are guaranteed that the destructor will be called, even if you get GPF or etc. I personally always use asynchronous exception handling, but unfortunately, the default is synchronous. To enable asynchronous exception handling, either find it in the compiler settings, or put the /EHa flag.
To get immunity against memory/resource leaks, never let your pointers/handles unguarded. For example, don't write it this way.
PBYTE pPtr = new BYTE[nSize];
delete[] pPtr;
Better to write it the following way:
GPtr_T<BYTE> pPtr = new BYTE[nSize];
If you want your function to return a pointer, then instead of writing it this way:
PBYTE AllocMyArr()
{
PBYTE pPtr = new BYTE[nSize];
return pPtr;
}
You better rewrite it in the following way:
void AllocMyArr(GPtr_T<BYTE>& pPtr)
{
pPtr = new BYTE[nSize];
}
That is, don't leave pointers unattended.
Another important note: sometimes, it is good to override the GObj_T
/GRef_T
instead of just instantiating it via typedef
. This allows to add some extra functionality that was not designed in the base class. For example, we may want the file handle wrapper to have Write
and Read
member functions for convenience.
There is a very serious pitfall here: Suppose you write something like this:
class HFile_G : public GObj_T<GBaseH_FileClose>
{
public:
HFile_G() {}
HFile_G(HANDLE hFile) : GObj_T<GBaseH_FileClose>(hFile) {}
void Write( ... );
DWORD Read( ... );
};
HFile_G hFile;
hFile = CreateFile( ... );
hFile = CreateFile( ... );
Guess what this will do. You won't believe!
Do you expect that on the first CreateFile
we save the returned handle, and at the second call, we close the previous handle and save the new one? Wrong!
At the inherited class, we didn't implement the assignment operator that takes a HANDLE
. It exists in our base class, but according to C++ rules, it is not automatically inherited. But, this is not the worst thing: the compiler won't give you an error here. This is what it will do instead:
- Create a temporary
HFile_G
object from the handle you pass. This is possible because we have the appropriate constructor. - Assign our object to this temporary one. How can this be possible if we didn't implement it? Don't worry, the compiler generates the assignment operator automatically. How? By just copying the members (!!!). So that our previously saved handle is overwritten by the new one.
- As we said, the
HFile_G
object generated by the compiler is temporary, its lifetime is the assignment statement. So, immediately after the assignment, the compiler calls its destructor. Hence, the new handle is closed.
Impressing, isn't it? Instead of closing the old handle, we'll close the new one! And you have no errors/warnings. You just get the surprise at the runtime. In my opinion, it is an extremely stupid idea to generate an assignment operator / copy constructor automatically, especially if your base class is a non-trivial one. But this is the reality.
To avoid such situations, you must implement assignment and copy constructor in the inherited class. Furthermore, you have to implement (or disable by declaring it private
) three assignment operators: one that takes a HANDLE
, one that takes a const reference to the base class, and one that takes a reference to your class. The same applies to constructors.
I've also pointed on this issue in the comments in the code. To make the inherited class work correctly, you can write it the following way:
class HFile_G : public GObj_T<GBaseH_FileClose>
{
INHERIT_GUARD_OBJ(HFile_G, GObj_T<GBaseH_FileClose>, HANDLE)
void Write( ... );
DWORD Read( ... );
};
This macro will implement all the problematic functions.
This is all. Hope you'll find this useful. Comments are appreciated.