I decided to talk a little about memory management operators from native C++ because there are some thoughts and practices that I keep in my mind.
Well, I completely ignore the defaults new/delete, I pretend that these things don’t exist in the language, that’s not only because the C++ guidelines and good practices encourages you to keep object resource well defined and automatic, it’s also a portability thing, game engines needs to use the memory management functions provide by the target platform, otherwise, things may not work properly or just crash because the alignment for that platform/architecture was not set.
Here’s a simple object:
struct Object
{
Object(const char* Name, const uint32_t Flags = 0)
: Flags{Flags}
{
strcpy(this->Name, Name);
}
char Name[64] = { 0 };
uint32_t Flags = 0;
};
In the main file we create it:
int main()
{
Object* lNewObject = new Object("My name");
delete lNewObject;
return EXIT_SUCCESS;
}
The Object creation is not clear if was user defined or it uses default C++ implementation. Of course, it is an abstraction, but I don’t think it is a good practice since the user can simply assume that a operator new C++ implementation will be called and this can be wrong.
Another thing is there’s no need to dynamic allocate memory for an object that don’t have virtual functions/destructor that can be accessed by its base pointer. So, what can be done is implement operator new with an assertion to prevent objects there aren’t polymorphic to be “new allocated”.
template < typename T >
struct PolymorphicNewDelete
{
static void* operator new(size_t Size)
{
static_assert(std::is_polymorphic<T>::value, "no need to dynamic allocate.");
return ::operator new(Size);
}
static void operator delete(void* Ptr)
{
static_assert(std::is_polymorphic<T>::value, "no need to free.");
::operator delete(Ptr);
}
};
struct Object : PolymorphicNewDelete<Object>
{
// PREVIOUS CODE
};
PolymorphicNewDelete implements operator new and operator delete with just “is_polymorphic” static asserts. And then using CRTL we can inherit to Object.
I prefer use placement operator new with functions create/destroy, I believe that it’s more direct and precise.
inline void* Allocate(size_t Size)
{
return malloc(Size); // Platform memory allocation function
}
inline void* Allocate(size_t Size, size_t Alignment)
{
(void)Alignment;
return malloc(Size); // Platform memory allocation function
}
inline void* Free(void* Ptr, size_t Size)
{
(void)Size;
return free(Ptr); // Platform memory deallocation function
}
So, here I made basic memory functions to call platform memory functions. The one that uses alignment is just the same as the one that don’t uses alignment, but we also can use the windows api _aligned_malloc and _aligned_free, for linux the posix_memalign or aligned_alloc.
template < typename T, typename ... TArgs >
T* Create(TArgs&&... Args)
{
static_assert(std::is_polymorphic<T>::value, "no need to dynamic allocate.");
void* Memory = Allocate(sizeof(T), alignof(T));
#ifndef NDEBUG
if(!Memory)
{
printf("Failed to allocate '%llu' bytes\n", sizeof(T));
return nullptr;
}
#endif
return new (Memory) T{std::forward<TArgs>(Args)...};
}
template < typename T >
void Destroy(T* Ptr)
{
static_assert(std::is_polymorphic<T>::value, "no need to free.");
if(Ptr)
{
Ptr->~T();
Free(Ptr, sizeof(T));
}
}
The Create function allocates memory for object of type T and construct it using operator placement new, but at first, it check if object of type T is polymorphic, in other words, Create function only compiles if object of type T was polymorphic. The Destroy function basically calls the destructor and frees memory, but only if object of type T was polymorphic.
template < typename T >
T* CreateArray(size_t Size)
{
T* lArray = static_cast<T*>(Allocate(sizeof(T)*Size, alignof(T)));
#ifndef NDEBUG
if(!lArray)
{
printf("Failed to allocate '%llu' bytes\n", sizeof(T));
return nullptr;
}
#endif
return std::uninitialized_default_construct_n(lArray, Size);
}
template < typename T >
void DestroyArray(T* Array, size_t Size)
{
if(Array)
{
std::destroy_n(Array, Size);
Free(Array, sizeof(T)*Size);
}
}
The CreateArray always allocates an C array and default construct all elements. The DestroyArray destructs all elements and free the memory.
A downside in CreateArray and DestroyArray functions is the user needs to specify always the count value for the array. If we pass zero or a value greater than the allocated array size to the DestroyArray function, there will be segmentation fault. We can use fat pointers to do it.
Fat pointers are basically pointers that have additional information carried by them.
template < typename T >
struct ArrayPtr
{
size_t Size;
T* Ptr;
};
template < typename T >
ArrayPtr<T> CreateArray(size_t Size)
{
/* CODE*/
}
template < typename T >
void DestroyArray(ArrayPtr<T> Array)
{
/* CODE*/
}
Here’s a very structured and explicit way to do it. A fat pointer to array pointer, that I will show, works basically like this but it is compact to just an allocation with count, data and, the data pointer is shifted.
template < typename T >
T* CreateArray(size_t Size)
{
const auto lSize = sizeof(T) * Size + sizeof(size_t);
auto lMemory = static_cast<size_t*>(Allocate(lSize));
#ifndef NDEBUG
if(!lMemory)
{
printf("Failed to allocate '%llu' bytes\n", sizeof(T));
return nullptr;
}
#endif
*lMemory = Size; // Set array size to first location in memory buffer
auto lArray = reinterpret_cast<T*>(lMemory + 1ull); // Advance the pointer in one unit of (size_t*)
return std::uninitialized_default_construct_n(lArray, Size);
}
Now CreateArray returns a fat pointer to target array, to get the array size we can just get previous size_t*.
template < typename T >
size_t ArraySize(T* Array)
{
return *(reinterpret_cast<size_t*>(Array) - 1ull);
}
So, the DestroyArray looks like this:
template < typename T >
void DestroyArray(T* Array)
{
if(Array)
{
const auto lSize = ArraySize(Array);
std::destroy_n(Array, lSize);
Free(Array, sizeof(T)*lSize);
}
}
It just get array size to be able to call destruction for the elements.
In conclusion, operator new/delete in its default form I don’t think that it is useful with you want control over your program, using functions like these above its more simple, controlled and explicit. Of course, there is no need to use fat pointers to allocate arrays or memory blocks, using structures with pointer and size makes the api even more explicit and simpler, I would say.