SatView: Pointer Perfect, Part 2: Construction / Destruction - Copying Pointers vs. Copying Memory
(Page 3 of 4 )
If a member variable points to a piece of memory, you can either copy the variable (i.e. the pointer) or the memory it points to. When you copy the pointer, the data instantiated in the memory becomes shared between different objects, giving nasty, unexpected side effects (such as memory exceptions). It is what we call a "shallow copy". When you copy the memory instead of the pointer, we call it a "deep copy".
It is important to know that when your class doesn’t define a copy constructor or assignment operator, the compiler will implicitly create these for you. Since it will always perform a bit wise copy, the copy will be shallow! So if your class contains pointers as member variables, make sure that you always declare a copy constructor and an assignment operator... even if it was just to declare them private to make the class non-copyable. E.g.:
class MyNonCopyable {
public:
MyNonCopyable();
~MyNonCopyable();
void foo();
private:
/* this hides the copy constructor. */
MyNonCopyable(MyNonCopyable const &);
/* this hides the assignment operator. */
MyNonCopyable& operator=(MyNonCopyable const&);
private:
MyObj *m_pMyObj;
};
A deep copy requires you to free the memory pointed at by the member variable first, before you acquire new memory to contain a copy of the data from rhs (otherwise your application would be leaking memory). When the object is assigned to itself, this would mean we would lose the data because we cannot copy the data from ourselves after it has been freed! Not exactly what we had in mind... therefore it can be useful to compare the address of rhs with the address of this to prevent this silly case from happening.
The following use of the this pointer should make you cringe or at least raise your eyebrows:
delete this;
Think about it! Do you realize the implications of this statement? I won’t tell you not to use it, but you must understand its consequences. It's all about when you use it.
When you are working with COM, you will run into this statement a lot. The simple reason for this is that classes have to be derived from the IUnknown interface, which forces classes to be reference counted. This means that you do not construct/destruct objects using new/delete, but that you must obtain pointers to their instances through the QueryInterface(REFIID, void**) function and ‘free’ them using Release(). You are no longer directly responsible for the memory allocated. Here's a code example:
HRESULT MyCOMClass::QueryInterface(REFIID riid, void** interface) {
if (riid == IID_IUnknown)
interface = static_cast<IUnknown*>(this);
else if (riid == IID_IMyInterface01)
interface = static_cast<IMyInterface01*>(this);
else if (riid == IID_IMyInterface02)
interface = static_cast<IMyInterface02*>(this);
else
interface = NULL;
if (interface) {
reinterpret_cast<IUnknown*>(*interface)->AddRef();
return S_OK;
}
else
return E_NOINTERFACE;
}
COM forces you to implement a pattern also known as “the extension interface.” This allows you to export multiple interfaces per component, which prevents bloating of these interfaces. It also makes it possible for you to easily extend and modify the functionality of these components, without breaking them. I will describe this pattern in more detail in the future.
The reference counting is done through the AddRef() and Release() functions. For example:
ULONG MyCOMClass::AddRef() {
return ++m_RefCount;
}
ULONG MyCOMCLass::Release() {
ULONG result = --m_RefCount;
if (result == 0)
delete this;
return result;
}
So, did you notice the delete this? Even though it is dangerous to use, it is quite necessary here.
You can easily introduce a bug here (and trust me... I've found them in COM books!) by writing the following code:
ULONG MyCOMClass::Release() {
if (--m_RefCount == 0)
delete this;
return m_RefCount;
}
Do you see the potential danger in this code? Think about what delete this does. It calls the destructor of the object of which we are currently executing a member function! This means that returning the contents of a member variable after that very same object has been destructed can yield unexpected results!
Sure, this will work fine during development, but don’t be surprised if it blows up in your client's production environment.
Potential Problems When Crossing DLL Boundaries
It is not possible to allocate memory in a DLL (either explicitly with malloc / new or implicitly with aforementioned strdup) and pass the pointer across the DLL boundary into the process space to free it, when the DLL and the application are using different copies of the CRT (C-RunTime) libraries. This will cause a nasty memory access violation or worse: corrupt the heap! When you bump into an assert in the CRT lib when it tries to execute free(), you will know you have run into this very problem.
Memory is only valid for the copy of the CRT where they were allocated. This means that when you are using two different CRT libraries (LIBCMT.LIB and MSVCRT.LIB for example) together, you cannot expect one to correctly free something the other has allocated. Because they most probably are using different heap managers, the heap runs the risk of becoming corrupted when you run the application in release mode! Pity the programmer who has to track the bug that crashes his application this way.
It seems like something trivial to avoid, but I can guarantee you will run into that assert in free() when you are using COM and utilizing SAFEARRAYs and BSTRs. Hopefully I have just saved you some future headaches.
Next: Pointer Pointers >>
More Code Examples Articles
More By J. Nakamura