Sign in to follow this  
groveler

Overriding C++ new and delete operators to make garbage collector

Recommended Posts

OK, i had an idea for a program I'm current having memory problems with (and i was being extremely careful with memory too!): Is there any way to override the new operator so that it allocates memory for the object, and then adds that object to a vector? Also, for the delete operator, can I override it to remove that object from the vector and then free the memory? The plan is, at the end of the program I loop through the vector to see what objects are still there and thereby find my memory leaks. I already know that you can override new and delete but am a bit iffy with the details. Any ideas on how to go about this? or is it a bad idea in general?

Share this post


Link to post
Share on other sites
I've recently been working on a memory manager just like you.

First of all i created a simple class to store the information of a memory block, including some details for logging purposes.

I added the following information:
- Filename of where the allocation was made
- Function name
- Line number
- The size of the memory block in bytes
- The address of the memory block

The previous 2 items can be easily obtained from the new operator


I then made a memory manager class, which i implemented as a singleton.
That class contained a std::map<void*, CMemoryBlock> list. I did this, so i could access the memory block data by the address which is accessible by the new and delete operators.

Lastly i overloaded the new operator and made sure the implementation follows the C++ standard.

It basicly worked like this:

void *operator new(size_t Size, std::string File, std::string Function, long Line)
{
// Allocate the block of memory
void* Addr = malloc(Size);
...

// Register the memory block
CMemoryManager::GetInstance()->RegisterMemBlock(File, Func, Line, Addr, Size);

return Addr;
}



Unfortunately the delete operator can't be overloaded, so i implemented delete as follows:


void operator delete(void* Addr)
{
// Release the memory
free(Addr);

// Unregister the memory block
CMemoryManager::GetInstance()->UnregisterMemBlock(Addr);
}



This is all working nicely... the last thing to do, was creating a macro for the new operator, so the file, function and line parameters got filled in automaticly.


#define MY_NEW new(__FILE__, __FUNCTION__, __LINE__)
#define new MY_NEW


The last thing you need to do is create a function which prints out the registered memory blocks when the program ends. I did this in the destructor of the memory manager class.

And there you have it, your own memory manager.
Hope this helps

Share this post


Link to post
Share on other sites
Quote:
Original post by groveler
wow!!!

thats almost exactly what im looking for!

just a quickie: the ... in your operator* new method, what goes there?

thanks a LOT for the reply... this will help tremendously!!!!!


You wanna look into overloading placement new operator there is some faqs about it here. There are a couple of things you need to think about, do you wont to overload for a particular user-defined type or for all types. If a particular user-defined type then you can overloaded it within the class declaration, if it's for all then overload placement new operator declared in header new.

Share this post


Link to post
Share on other sites
Guest Anonymous Poster
I'd just like to point out that while Garbage Collection is a subcategory of Memory Management, it does not seem to be the kind of Memory Management you want to do. Well, I guess you could call these Garbage Collectors, but 'real' Garbage Collectors free memory in 'realtime' as you no longer need it, while these things being discussed (from what I can tell) only free unfreed memory when the program exits. This is good for debugging and making sure there are no leaks, but it doesn't provide quite the same benefits as 'real' Garbage Collection.

-Extrarius

Share this post


Link to post
Share on other sites
I guess i can just post my code while i'm at it...

Maybe i can even get some feedback from the pro's :-)

Code for: Memory.h

// Inclusion Guard
#ifndef MEMORY_H_INCLUDED
#define MEMORY_H_INCLUDED


// System Includes
#include <windows.h>
#include <string>
#include <map>
#include <new>


// Macros
#ifdef _DEBUG
// Overloaded new and delete operators
#define DEBUG_NEW new(__FILE__, __FUNCTION__, __LINE__)
#else
// Default new and delete operators
#define DEBUG_NEW new
#endif


// Undefine the new and delete operators
#undef new
#undef delete


class CMemoryBlock
{
protected:
// Protected Functions

// Constructor
CMemoryBlock(std::string File, std::string Func, long Line, void* Addr, size_t Size);

// Output operator, used when writing the memory block to a file
friend std::ostream &operator<<(std::ostream &Out, CMemoryBlock &Rhs);


// File information
std::string m_File;
std::string m_Func;
long m_Line;

// Memory information
void* m_Addr;
size_t m_Size;
SYSTEMTIME m_Time;


private:
// Only memory manager can create instances of a memory block
friend class CMemoryManager;
};


template<class T>
class AutoPtr;

class CMemoryManager
{
public:
// Public Functions

// Returns a reference to an instance of the memory manager
static CMemoryManager &GetInstance();

// Writes all memory leaks to a file
void LogMemoryLeaks();


protected:
// Protected Functions

// Destructor
~CMemoryManager();

// Registers a memory block
void RegisterMemoryBlock(std::string File, std::string Func, long Line, void* Addr, size_t Size);

// Unregisters a memory block
void UnregisterMemoryBlock(void *Addr);


private:
// Private Variables

// List of registered memory block
std::map<void*, CMemoryBlock> m_MemoryList;


// Instance of the memory manager
static CMemoryManager m_Instance;


// Only the new and delete operators have access to the protected members
friend void *operator new(size_t Size, std::string File, std::string Func, long Line);
friend void *operator new[](size_t Size, std::string File, std::string Func, long Line);
friend void operator delete(void *Ptr);
friend void operator delete[](void *Ptr);
};


// Overloaded new operators
void *operator new(size_t Size, std::string File, std::string Func, long Line);
void *operator new[](size_t Size, std::string File, std::string Func, long Line);

// Overridden delete operators
void operator delete(void *Ptr);
void operator delete[](void *Ptr);


// Set the new operator
#define new DEBUG_NEW


#endif // MEMORY_H_INCLUDED
[/code]

Code for Memory.cpp
[code]
// System Includes
#include <direct.h>
#include <fstream>

// Project Includes
#include "Memory.h"


// Macros
#ifdef _DEBUG
// Assert which inserts a break point
#define Assert(x) if ( (x) == false ) __asm { int 3; }
#else
// Empty assert
#define Assert(x)
#endif


// Undefine the new and delete operators
#undef new
#undef delete



// Constructor
CMemoryBlock::CMemoryBlock(std::string File, std::string Func, long Line, void* Addr, size_t Size):
m_Func(Func),
m_Line(Line),
m_Addr(Addr),
m_Size(Size)
{
// Get the file name
char WDir[260];
getcwd(WDir, 260);
m_File = File.substr(strlen(WDir) + 1);

// Format the function name
m_Func += "()";

// Get the current time of when the allocation was made
GetLocalTime(&m_Time);
}


// Output operator, used when writing the memory block to a file
std::ostream &operator<<(std::ostream &Out, CMemoryBlock &Rhs)
{
Out << "--------------------------------------------------\n";
Out << "File:\t" << Rhs.m_File << "\n";
Out << "Func:\t" << Rhs.m_Func << "\n";
Out << "Line:\t" << Rhs.m_Line << "\n\n";
Out << "Date:\t" << Rhs.m_Time.wDay << "-" << Rhs.m_Time.wMonth << "-" << Rhs.m_Time.wYear << "\n";
Out << "Time:\t" << Rhs.m_Time.wHour << ":" << Rhs.m_Time.wMinute << ":" << Rhs.m_Time.wSecond << " - " << Rhs.m_Time.wMilliseconds << "\n\n";
Out << "A memory block of " << (int)Rhs.m_Size << " bytes has not been properly deleted.\nThe memory still exists on memory address " << Rhs.m_Addr << "\n";
Out << "--------------------------------------------------\n\n\n";
return Out;
}


// Public Functions

// Returns a reference to an instance of the memory manager
CMemoryManager &CMemoryManager::GetInstance()
{
return m_Instance;
}


// Writes all memory leaks to a file
void CMemoryManager::LogMemoryLeaks()
{
// Iterator
std::map<void*, CMemoryBlock>::iterator i;

// Open the file for reading
std::ofstream fout("Memory.log");
if ( fout.is_open() )
{
// Check if any memory leaks have been found
if ( m_MemoryList.size() == 0 )
fout << "No memory leaks have been detected.";
else
// Write all memory blocks to the log
for ( i = m_MemoryList.begin(); i != m_MemoryList.end(); ++i )

// Write the memory block to the file
fout << i->second;

// Close the file for writing
fout.close();
}
}


// Protected Functions

// Destructor
CMemoryManager::~CMemoryManager()
{
// Write the memory leaks to a file
LogMemoryLeaks();

// Open the log file
ShellExecute(NULL, "open", "Memory.log", NULL, NULL, SW_SHOWDEFAULT);
}


// Registers a memory block
void CMemoryManager::RegisterMemoryBlock(std::string File, std::string Func, long Line, void* Addr, size_t Size)
{
// Add the block of memory to the list
m_MemoryList.insert(std::make_pair(Addr, CMemoryBlock(File, Func, Line, Addr, Size)));
}


// Unregisters a memory block
void CMemoryManager::UnregisterMemoryBlock(void *Addr)
{
m_MemoryList.erase(Addr);
}


// Instance of the memory manager
CMemoryManager CMemoryManager::m_Instance;



// Overloaded new operators
void *operator new(size_t Size, std::string File, std::string Func, long Line)
{
// Make sure a valid size is being allocated
if ( Size == 0 ) Size = 1;

// Allocate a piece of memory
void *Address = NULL;
while ( !(Address = malloc(Size)) )
{
new_handler Handler = std::set_new_handler(0);
std::set_new_handler(Handler);

if ( Handler)
Handler();
else
throw std::bad_alloc();
}

// Register the newly created memory block
CMemoryManager::GetInstance().RegisterMemoryBlock(File, Func, Line, Address, Size);

// Return the memory address
return Address;
}


void *operator new[](size_t Size, std::string File, std::string Func, long Line)
{
// Make sure a valid size is being allocated
if ( Size == 0 ) Size = 1;

// Allocate a piece of memory
void *Address = NULL;
while ( !(Address = malloc(Size)) )
{
new_handler Handler = std::set_new_handler(0);
std::set_new_handler(Handler);

if ( Handler)
Handler();
else
throw std::bad_alloc();
}

// Register the newly created memory block
CMemoryManager::GetInstance().RegisterMemoryBlock(File, Func, Line, Address, Size);

// Return the memory address
return Address;
}


// Overridden delete operator
void operator delete(void *Ptr)
{
if ( Ptr )
{
// Unregister the memory block, because it has been properly deleted
CMemoryManager::GetInstance().UnregisterMemoryBlock(Ptr);

// Release the memory
free(Ptr);
}
}


// Overridden delete operator
void operator delete[](void *Ptr)
{
if ( Ptr )
{
// Unregister the memory block, because it has been properly deleted
CMemoryManager::GetInstance().UnregisterMemoryBlock(Ptr);

// Release the memory
free(Ptr);
}
}



I suggest you study this piece of code and try to implement your own version and don't just copy and paste.
Only refer to my code when you're stuck, this way you will learn the most.

Please let me know if you have any questions. I'll be glad to help.

Joost.

[note]
Washu: Changed code tags to source tags.
[/note]

[Edited by - Washu on August 4, 2004 7:33:07 PM]

Share this post


Link to post
Share on other sites
Stupid internall 500 errors.. for the 2nd time... lets try this again...



Included are my logging and memory tracking files. Simply Include "MyMem.h" and it will write out the allocations/deallocations to a log file specified in the Logger.Init call. It tracks the address and size of each allocation, reports at the end of the program if there is any memory not deleted, automatically deletes it for you so you don't have any leaks (but you don't want to rely on it!), and also tells you the maximum amount of memory you're program allocated at one time :). It does NOT count it's own memory however, so it will be an accurate value whether you're using it or not (well, accurate as in telling you how much you're program is allocating).


/* MyMem.h */
#ifndef MyMem_H
#define MyMem_H

//Set our memory to debuf if we're in debug mode build!
//Uncomment to let it debug memory only in debug builds (in msvc anyways)
//#ifdef _DEBUG
#define Mem_Debug
//#endif

#ifdef Mem_Debug

#include "Logger.h"

struct MemList_S
{
void *Location;
unsigned long Size;
MemList_S *prv, *nxt;
};

class MemTracker_C
{
public:
MemList_S *MemList_Head;
unsigned long MemCounter;
unsigned long MemAllocated;
unsigned long MaxAllocated;
Logger_C Logger;
public:
MemTracker_C(void);

void Add(void *ptr, unsigned long Size);
void Remove(void *ptr);

~MemTracker_C(void);
};

void *operator new(size_t size);
void operator delete(void *ptr);
void *operator new[](size_t size);
void operator delete[](void *ptr);

extern MemTracker_C MemTracker;

#endif
#endif




/* MyMem.cpp */
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include "MyMem.h"
#ifdef Mem_Debug

MemTracker_C MemTracker;

MemTracker_C::MemTracker_C(void)
{
MemCounter=0;
MemAllocated=0;
MaxAllocated=0;

MemList_Head = 0; //Set us to NULL

//Clear our file out
Logger.Init("Memory.log");
Logger.Write("Memory Tracker Started...");
}

void MemTracker_C::Add(void *ptr, unsigned long size)
{
MemList_S *ptrMemObject;
ptrMemObject = (MemList_S*)malloc(sizeof(MemList_S)); //Allocate an object!
ptrMemObject->Location = ptr;
ptrMemObject->Size = size;
MemAllocated += size;
if (MemAllocated > MaxAllocated)
MaxAllocated=MemAllocated;
if (!MemList_Head) //Nothing yet!
{
ptrMemObject->nxt = ptrMemObject;
ptrMemObject->prv = ptrMemObject;
MemList_Head = ptrMemObject;
}
else //Otherwise, lets add it to the end!
{
ptrMemObject->nxt = MemList_Head;
ptrMemObject->prv = MemList_Head->prv;

MemList_Head->prv->nxt = ptrMemObject;
MemList_Head->prv = ptrMemObject;
}
Logger.Write("Added - %x %u",(unsigned long)ptr,size);
}

void MemTracker_C::Remove(void *ptr)
{
if (MemList_Head) //We have a list?
{
if (MemList_Head->Location == ptr) //first one?
{
if (MemList_Head->nxt == MemList_Head) //Last one in list?
{
MemAllocated-= MemList_Head->Size;
Logger.Write("Removed - %x %u",MemList_Head->Location,MemList_Head->Size);
free(MemList_Head);
MemList_Head = 0;
}
else //We have others in our list
{
MemAllocated-= MemList_Head->Size;
Logger.Write("Removed - %x %u",MemList_Head->Location,MemList_Head->Size);
MemList_S *ptrMemList = MemList_Head->nxt;
MemList_Head->nxt->prv = MemList_Head->prv;
MemList_Head->prv->nxt = MemList_Head->nxt;

free(MemList_Head);
MemList_Head = ptrMemList;
}
}
else //Ok, this isn't our first one!
{
MemList_S *ptrMemList = MemList_Head->nxt;
while (ptrMemList!=MemList_Head)
{
if (ptrMemList->Location == ptr)
{
MemAllocated-= ptrMemList->Size;
Logger.Write("Removed - %x %u",ptrMemList->Location,ptrMemList->Size);
ptrMemList->nxt->prv = ptrMemList->prv;
ptrMemList->prv->nxt = ptrMemList->nxt;

free(ptrMemList);
break;
}
ptrMemList = ptrMemList->nxt;
}
}
}
}

MemTracker_C::~MemTracker_C(void)
{
if (MemCounter!=0)
{
Logger.Write("\n*** Summary ***");
Logger.Write("Final memory allocated %d - %d bytes",MemCounter,MemAllocated);
while (MemList_Head) //While we have memory allocated!
{
Remove(MemList_Head->Location); //Lets remove it from our list!
}
}
else
Logger.Write("\n*** Summary *** - All memory accounted for!");
Logger.Write("Maximum memory allocated %d bytes",MaxAllocated);
MemCounter = 0;
}

void *operator new(size_t size)
{
void *ptr = malloc(size);
++MemTracker.MemCounter;
MemTracker.Add(ptr,size);
return ptr;
}

void operator delete(void *ptr)
{
if (MemTracker.MemCounter)
{
--MemTracker.MemCounter;
MemTracker.Remove(ptr);
}
free(ptr);
}

void *operator new[](size_t size)
{
void *ptr = malloc(size);
++MemTracker.MemCounter;
MemTracker.Add(ptr,size);
return ptr;
}

void operator delete[](void *ptr)
{
if (MemTracker.MemCounter)
{
--MemTracker.MemCounter;
MemTracker.Remove(ptr);
}
free(ptr);
}

#endif




/* Logger.h"
#ifndef Logger_H
#define Logger_H

#include <stdio.h>

class Logger_C
{
public:
FILE *LogFile;
char Enabled;
char *LogFileName;

public:
Logger_C(void);
void Init(char *fName);
void Write(const char *str, ...);
~Logger_C(void);
};

#endif




/* Logger.cpp */
#include "Logger.h"
#include <stdarg.h>

Logger_C::Logger_C(void)
{
Enabled = 0;
}

void Logger_C::Init(char *fName)
{
Enabled = 1;
LogFileName = fName;
LogFile = fopen(LogFileName,"wb"); //Clear file
fclose(LogFile); //Close our file
Write("Logging system started");
}

void Logger_C::Write(const char *str, ...)
{
va_list args;
if (Enabled && LogFileName)
{
va_start(args,str);
LogFile = fopen(LogFileName,"awb"); //append write binary
vfprintf(LogFile,str,args);
fprintf(LogFile,"\n");
fclose(LogFile);
va_end(args);
}
}

Logger_C::~Logger_C(void)
{
}



[Edited by - Ready4Dis on August 4, 2004 8:19:03 PM]

Share this post


Link to post
Share on other sites
Stop reinventing the wheel guys. If a few people have thought of the same thing the chances are it's been researched to death already. This is the first link on google for "c++ garbage collection":
http://www.hpl.hp.com/personal/Hans_Boehm/gc/

Share this post


Link to post
Share on other sites
Oh, by the way, there is even a way to get the file/line that the memory was allocated and deallocated on, that way you can really narrow it down. On exit you could have it print which memory wasn't deleted, and what file and line # it was allocated on, makes it easier for fixing memory leaks as you know where they are originating :).

Share this post


Link to post
Share on other sites
I am getting a link error with Ready4Dis's manager.
LINK : fatal error LNK1104: cannot open file "Opcode_D.lib"
Do I have a setting wrong in my project?
I am using VC++ 6.0 with sp6 if that makes a difference.
Anyone know what this is talking about? The file doesn't exist on my system...
Thanks,
Steele.

Share this post


Link to post
Share on other sites
Quote:
Original post by seanw
Stop reinventing the wheel guys. If a few people have thought of the same thing the chances are it's been researched to death already. This is the first link on google for "c++ garbage collection":
http://www.hpl.hp.com/personal/Hans_Boehm/gc/
I haven't examined it in great detail, but I'm pretty sure the boehm gc isn't as fast as it could be because it manually scans memory instead of keeping track of everything with smart pointers. Yes, they would be a pain to use in the code, but they could greatly speed up the GC because it wouldn't need to scan anything except the limited class of pointers(I'm guessing there would be many classes of pointers for specific attributes, such as 'top level pointers', 'pointers that are a class member', 'function-local pointers', etc or something like that) it needs to.

Share this post


Link to post
Share on other sites
Ready4Dis & cozman your memory managers are both simillar but i can see one slight problem in both, the C memory functions deal with uninitialized memory so malloc should be fine inside the new operator as the constructor is called after the memory is allocated but free does not invoke the deconstructor so there might be some problems there. You probably need to explicitly invoke the deconstructor before you free the memory.

Share this post


Link to post
Share on other sites
OK, I've been having major problems posting this......

If your using MSVC then you can save your self a lot of trouble by using the debugging functions that are already provided in the CRT. Here's some source to show how to use it:

#include <windows.h>
#include <crtdbg.h>

//*********************************************************************************
// main
//*********************************************************************************
//
// Parameters: argc = the number of arguments
// argv = pointer to array of arguments
//
// Function: Entry point for applications
//
// Return: Application exit code
//
//*********************************************************************************

int main
(
int argc,
char *argv []
)
{
#define SET_FLAG(value,flag) (value) |= (flag)
#define CLEAR_FLAG(value,flag) (value) &= ~(flag)

int
flag;

flag = _CrtSetDbgFlag (_CRTDBG_REPORT_FLAG);

// if set, enable debug heap allocations
SET_FLAG (flag, _CRTDBG_ALLOC_MEM_DF);
// is set, call _CrtCheckMemory at every allocation and deallocation (slow)
CLEAR_FLAG (flag, _CRTDBG_CHECK_ALWAYS_DF);
// if set, include CRT allocations as well (will always generate some leaks at program exit)
CLEAR_FLAG (flag, _CRTDBG_CHECK_CRT_DF);
// if set, keep freed memory blocks in the heap’s linked list
CLEAR_FLAG (flag, _CRTDBG_DELAY_FREE_MEM_DF);
// if set, perform automatic leak checking at program exit via a call to _CrtDumpMemoryLeaks
SET_FLAG (flag, _CRTDBG_LEAK_CHECK_DF);

_CrtSetDbgFlag (flag);

// This causes a break when the specified allocation is made
if (argc == 2)
{
_CrtSetBreakAlloc (atoi (argv [1]));
}

// create a memory leak
malloc (100);

return 0;
}

Running this with no arguments produces the following output in MSVC's debug window:

Detected memory leaks!
Dumping objects ->
{28} normal block at 0x007801F0, 100 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

The '{28}' is the allocation number that wasn't freed. Running the application again with the command line argument '28' will causes the debugger to halt the program when allocation number '28' is made. You can then see exactly where the memory was allocated by stepping up the stack.

On a side note, it's a really good idea to make any memory manager work the same in debug builds as it does in release builds, i.e. store any debug information somewhere safe. Quite often problems occur when switching to release builds when stray pointers overwrite unused memory in debug versions but overwrite critical memory in release mode.

Skizz

[Edited by - Skizz on August 5, 2004 5:17:56 AM]

Share this post


Link to post
Share on other sites
Quote:
Original post by snk_kid
Ready4Dis & cozman your memory managers are both simillar but i can see one slight problem in both, the C memory functions deal with uninitialized memory so malloc should be fine inside the new operator as the constructor is called after the memory is allocated but free does not invoke the deconstructor so there might be some problems there. You probably need to explicitly invoke the deconstructor before you free the memory.


Nope, you don't. Go take it for a test, the constructor and deconstructor both get called, definetly not a problem.

Share this post


Link to post
Share on other sites
Quote:
Original post by Ready4Dis
Quote:
Original post by snk_kid
Ready4Dis & cozman your memory managers are both simillar but i can see one slight problem in both, the C memory functions deal with uninitialized memory so malloc should be fine inside the new operator as the constructor is called after the memory is allocated but free does not invoke the deconstructor so there might be some problems there. You probably need to explicitly invoke the deconstructor before you free the memory.


Nope, you don't. Go take it for a test, the constructor and deconstructor both get called, definetly not a problem.


I guess thats a property of the delete operator because normally free doesn't call the deconstructor before freeing the memory so everything is cool then.

Share this post


Link to post
Share on other sites
Quote:
Original post by Extrarius
I haven't examined it in great detail, but I'm pretty sure the boehm gc isn't as fast as it could be because it manually scans memory instead of keeping track of everything with smart pointers. Yes, they would be a pain to use in the code, but they could greatly speed up the GC because it wouldn't need to scan anything except the limited class of pointers(I'm guessing there would be many classes of pointers for specific attributes, such as 'top level pointers', 'pointers that are a class member', 'function-local pointers', etc or something like that) it needs to.


But don't you think countless people before who have researched and developed garbage collectors would have thought about doing this if it was so obvious? Big companies like Sun for Java and MS for .NET will have researched garbage collection before picking a method and I'd be gobsmacked if you could better all their work by tinkering around. I'm not saying you shouldn't, but you might find it more rewarding to study previous work instead of reinventing the wheel. Like have you actually tried the Boeh GC? It's very good.

Share this post


Link to post
Share on other sites
Guest Anonymous Poster
seanw: I dont doubt that it is good, I just think that my priorities are different than theirs. I think all garbage collectors out there are concentrating primarily on making it easy to use with no hastle (and, within that constraint, makign it very fast), while the smart pointer approach I was talking about is annoying because you have to do extra work to use it, but it should be faster. I'm not looking for a GC so that my C++ code can be GC, but so that I can easily use it in C++ to make my scripting language have GC (so certain C++ classes related to the scripting need to be GCed, but I'll still be using manual memory management where appropriate such as classes that need the memory to persist exactly as long as they do where I can use constructor/destructor to handle it properly).

Implementation details always depend on priorities.

-Extrarius

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this