Be Carefully Consistent With Memory

One of the things that most C/C++ programmers start to take for granted is memory. It’s always there, and when used properly, it always “just works.” However, frameworks throw a bit of a monkey wrench into the equation because they are their own, separate entity. As framework designers, we have to be careful with the way we expose memory to the consumer lest we shatter that illusion that memory “just works.”

The first thing I want to discuss is the take home point I want you to leave with. When designing a framework, pick a convention and consistently stick to it. If you want the framework to allocate and return memory to the consumer, then make sure the framework is also what deallocates that memory. If you want the consumer to always pass in memory to the framework, then make sure the consumer is what frees that memory. If you want to expose reference counted objects (modeled after frameworks like CoreFoundation), that’s fine too. The key here is never to mix and match the conventions. Having the framework create memory that the consumer frees is asking for trouble!

Let’s take a look at why the consistency is critical. There are some obvious things to pick on, such as the framework calling malloc and the consumer calling delete. But let’s assume that your framework has clear documentation and the consumer is following it to the letter.

Let’s say your framework has a function like this:

// This function returns newly allocated memory which you 
// should release by calling the CRT's ::free function.
char *CreateStringFromNumber( int i );

and the consumer dutifully works with the function like this:

char *num = CreateStringFromNumber( 12 );
::printf( "The number was %s\n", num );

// We must free the string ourselves using free
::free( num );

Everything’s kosher, right?

Well, maybe. The truth is, you don’t know. As a framework designer, no matter what you do, you cannot guarantee that this situation will work. As a consumer, you can align the stars such that it will work. But most consumers won’t even realize they have to! The problem boils down to one of encapsulation.

When you create a framework, you are making an executable object that stands on its own. So the rules being followed in the framework are its own rules, separate from the application rules of consumers that use the framework. This is where the true issue sneaks in.

If the framework has statically linked in the CRT for some reason (for instance, by using the /MT or /MTd compiler options in VC++), and the application also statically links in its own CRT, then you can have a conflict of implementations. The version of malloc called by the framework may be different from the version of free called by the application. Or, if the framework dynamically links in the CRT but the application statically links it in, you can run into the same situation. The only way for this situation to work is for the CRT to be dynamically linked in to both the framework and the application — but there’s no way as a framework designer for you to enforce this!

A similar situation happens when using operator new and operator delete as the memory scheme. For instance, let’s say that the framework exposes the following function:

// This function returns memory allocated with new[], so 
// you must call delete[] when you wish to release it.
char *MakeACharacterArray( void );

and the application abides by the rules like this:

char *str = MakeACharacterArray();
// Do something with str, then delete it according to
// the framework's rules
delete [] str;

You still run into the malloc/free issue described above (after all, the default operators end up using malloc and free anyways). But you also may not realize that custom new and delete functions can come into play as well.

Let’s say the framework has a custom allocator for small chunks of data, but uses the default allocator for larger chunks. Then it may have its own operator new/new[] and operator delete/delete[] implementations. These implementations are not shared with the application! The converse is true as well, and neither the framework nor the application has any way to know about the custom allocators used in the other. As a simple example:

// Framework implementation
#include <malloc.h>
#include <stdio.h>

void *operator new( size_t count ) {
	::printf( "Helper new\n" );
	return ::malloc( count );
}

void operator delete( void *ptr ) {
	::printf( "Helper delete\n" );
	::free( ptr );
}

void *operator new[]( size_t count ) {
	::printf( "Helper new[]\n" );
	return ::malloc( count );
}

void operator delete[] ( void *ptr ) {
	::printf( "Helper delete[]\n" );
	::free( ptr );
}

extern "C" __declspec( dllexport ) void *GetSomeMemory( void )
{
	return new char[ 1024 ];
}

// Application implementation
#include <malloc.h>
#include <stdio.h>

void *operator new( size_t count ) {
	::printf( "Main new\n" );
	return ::malloc( count );
}

void operator delete( void *ptr ) {
	::printf( "Main delete\n" );
	::free( ptr );
}

void *operator new[]( size_t count ) {
	::printf( "Main new[]\n" );
	return ::malloc( count );
}

void operator delete[] ( void *ptr ) {
	::printf( "Main delete[]\n" );
	::free( ptr );
}

extern "C" __declspec( dllimport ) void *GetSomeMemory( void );

int main( void )
{
	char *memory = (char *)GetSomeMemory();

	delete [] memory;

	return 0;
}

If you create an example project where the framework is a DLL (it requires VC++ because of the __declspec stuff, but you can easily adapt it for gcc, et al) and the application consumes that DLL, you will see the following on the command line:

Helper new[]
Main delete[]

So my recommendation is to always be consistent with your memory allocations and deallocations — this allows you to avoid the sort of issues I’ve raised here. If you allocate it and deallocate it, then you can control how both ends behave. I would not leave that up to chance, as it can cause very subtle bugs and only in certain circumstances. Those are never fun issues to debug!

This entry was posted in Framework Design and tagged , , . Bookmark the permalink.

1 Response to Be Carefully Consistent With Memory

  1. Rajendra says:

    In addition, mixing new/free and malloc/delete. Application does not know which way the framework has allocated memory (malloc or new), so as a user of the framework, one should never try to deallocate memory acquired from the framework or any third party library. For example: Some framework is in C and the application is in C++, and you delete/delete[] after acquiring memory from the framework OR some framework is in C++ and the application is in C and you free memory acquired from the framework. It always makes sense to do not mix responsibility, i.e., separation of responsibility: (1.) I allocate, I deallocate (2.) You allocate, you deallocate.

Leave a Reply

Your email address will not be published. Required fields are marked *