Manual construction and destruction

Started by
35 comments, last by Servant of the Lord 11 years, 5 months ago
For the first time in 7 years of C++ programming, I think I need to use explicit constructor and destructor calls. dry.png

I am finding myself needing (more out of convenience and code cleanliness than necessity) a specific type of container class that isn't already a part of the standard library. Like vector, since my container is resizable, I want to have more memory available than is actually currently being used. std::vector has the current 'size' and also the 'capacity' to grow without reallocations. When vector has the memory reserved, the reserved memory isn't automatically constructed as that could have undesired side-effects, or simply just not be possible if the templated type didn't have a default constructor.

So I basically have everything already written out, except I'd like some clarity about explicitly calling the constructors and destructors.

Question 1:
For allocating memory, without calling the constructors in the memory, which of these are considered better practice/more-common:
unsigned char *memory = new unsigned char[num * sizeof(MyType)];
void *memory = ::operator new(num * sizeof(MyType));

Is this the time and place to use malloc() and free()?
Should I store the pointer as void* or unsigned char*?

Why unsigned char* over char*?

If I access a void* using the subscript operator[], will [n] access (n * sizeof(unsigned char)) or (n * sizeof(void*)) into that block of memory?
(I would assume sizeof(void*), but if void* is used for raw data access, I'd suspect people would want to increment a byte at a time)

Question 2:
Is this the correct way to manually call the constructor on raw memory?
new (&memory[atByte]) MyType(constructorArguments);

Is placement-new the only way?

Is this the correct way to destruct the object?
memory[atByte].~MyType();

And I can turn around and reconstruct it (with placement new again) to reuse the memory location?

Question 3:
How am I supposed to free the raw memory? Just delete[]?

But I have to run through it and destructor all the objects that haven't been destructed yet, right?
But I can't destruct any objects that haven't been constructed yet, right?

[hr]

Any other tips or traps to watch out for?

Advertisement

For allocating memory, without calling the constructors in the memory, which of these are considered better practice/more-common:
unsigned char *memory = new unsigned char[num * sizeof(MyType)];
void *memory = ::operator new(num * sizeof(MyType));


I'd say void * is more common, but the standard is specifically written so that the char * options is valid.

Is this the time and place to use malloc() and free()?
[/quote]
You can if you want, but ::operator new fits into C++ memory management better (e.g. throwing an exception on out of memory).

Should I store the pointer as void* or unsigned char*?
[/quote]
void * is more common, but a char pointer can be more convenient if you want to do pointer arithmetic.

Why unsigned char* over char*?
[/quote]
There's no reason to choose one over the other.

If I access a void* using the subscript operator[], will [n] access (n * sizeof(unsigned char)) or (n * sizeof(void*)) into that block of memory?
[/quote]
No, it should fail to compile. void is a incomplete type and so pointer arithmetic on void pointers is invalid.

Is this the correct way to manually call the constructor on raw memory?
new (&memory[atByte]) MyType(constructorArguments);
[/quote]
If memory is a char pointer or array that would work. If it's a void pointer then it should fail to compile.

Is placement-new the only way?
[/quote]
No, the standard library contains a number of mechanisms to create objects out of uninitialized memory. In particular you can look at allocators' construct() and destroy() members and std::uninitialized_copy() and std::uninitialized_fill().


Is this the correct way to destruct the object?
memory[atByte].~MyType();
[/quote]
That would depend on the type of memory, but probably not. You would want to call the destructor on a MyType pointer. Also the verb here is destroy not destruct.


And I can turn around and reconstruct it (with placement new again) to reuse the memory location?
[/quote]
Yes.


How am I supposed to free the raw memory? Just delete[]?
[/quote]
That would depend on how you allocated it. If you used new char[] you would use delete [] on a char pointer. If you used malloc(), call free(), etc.

But I have to run through it and destructor all the objects that haven't been destructed yet, right?
But I can't destruct any objects that haven't been constructed yet, right?
[/quote]
Unless the destructor is trivial, then you'll want to call the destructor for all constructed objects, and no, you shouldn't destroy objects that weren't constructed.


Any other tips or traps to watch out for?
[/quote]
That would be easier to say if you gave a higher level overview of what you're trying to accomplish in addition to how you want to accomplish it.
Can you store it as a [font=courier new,courier,monospace]T*[/font] ? Doing so would make indexing the array easier.

When manually allocating memory for a type, you need to ensure that your allocation is naturally aligned for that type. The easiest solution is to just ensure that all allocations are 16-byte aligned as a worst-case value, but that depends on the platform... C++11 added [font=courier new,courier,monospace]alignof[/font] to help here.

There's also platform/OS-specific options besides [font=courier new,courier,monospace]new[/font]/[font=courier new,courier,monospace]malloc[/font], such as [font=courier new,courier,monospace]_aligned_malloc[/font].
Thanks, SiCrane, I think I understand it.

I have a few more questions though! What about moving constructed objects between two locations in memory?

If moving an element to a new location in memory, std::move() would not be the right choice, would it? std::move() doesn't actually move memory, right? It's an unrelated topic, repossessing already-used memory but for a different variable of the same type (to avoid an uneccesary construction, assignment, and destruction)?

Should I use memcpy() to blast the bits between two different locations in memory?
After using memcpy(), I don't need to destruct the old memory location, right?
Before using memcpy, I don't need to construct the new memory location, right?

Basically, is this correct:
//Allocate memory.
Type *memoryA = (Type*) operator new(sizeof(Type) * 3);

//Construct type.
new (&memoryA[1]) Type();

//Allocate new memory.
Type *memoryB = (Type*) operator new(sizeof(Type) * 4);

//Copy data. The constructed object is now at memoryB[2].
memcpy(&memoryB[1], &memoryA[0], sizeof(Type) * 3);

//Delete old location (without destructing).
delete memoryA; //How's it know how much memory to delete? Do I need to cast back to void* before calling delete?

//Destruct type.
memoryB[2].~Type();

//Delete data.
delete memoryB;


How would "delete memoryA" know how much memory to delete? Do I need to cast back to void* before calling delete in that situation?

[Edit:] Yes, I can store it as Type*, don't know why I was thinking char*. Probably from data packets and file reading where the data isn't a uniform type.

//Delete old location (without destructing).
delete memoryA; //How's it know how much memory to delete? Do I need to cast back to void* before calling delete?


If you want to know that in detail, try showing the memory right before your allocated chunk. Chances are your compiler placed all the memory management info right there.

An easier answer: in the same way free knows how much memory to free when pointed at malloc'd memory.
f@dzhttp://festini.device-zero.de
You cannot use std::memcpy() unless the data type is "Plain old data".


How would "delete memoryA" know how much memory to delete?
[/quote]
You are mismatching - you need to use operator delete here. Vanilla delete is a combination of calling the destructor and de-allocating the memory.

As for how it knows, that is up to the underlying allocator.

When manually allocating memory for a type, you need to ensure that your allocation is naturally aligned for that type. The easiest solution is to just ensure that all allocations are 16-byte aligned as a worst-case value, but that depends on the platform... C++11 added [font=courier new,courier,monospace]alignof[/font] to help here.

Note that the standard requires the result of new char[], ::operator new and malloc() to be properly aligned to construct any standard type, as it was understood that one typical use case for any of these would be constructing an object in the returned memory. This unfortunately does not extend to non-standard types such as types declared with __declspec(align) or __m128 on MSVC. Basically, if your type doesn't use any double underscores then you don't need to worry about alignment when directly using dynamically allocated uninitialized memory.

How am I supposed to free the raw memory? Just delete[]?

If you allocate with [font=courier new,courier,monospace]::operator new()[/font], you have to deallocate with [font=courier new,courier,monospace]::operator delete()[/font].

Stephen M. Webb
Professional Free Software Developer

I will post some code with explanations that should answer all your questions and cover the tricky spots out for which you need to watch. This is taken from my CVector<> template, and the key parts are:
Allocating the list of elements (copying them to a new location)
Inserting items
Removing items
Destroying the whole list of items


There are also 2 helper functions:
LSVOID LSE_CALL Construct( LSUINT32 _tIndex ) {
new( &m_ptData[_tIndex] ) _tType;
}

LSVOID LSE_CALL Destroy( LSUINT32 _tIndex ) {
// This gives warning C4100 when this class is created with types that have no destructor,
// claiming _tIndex is unreferenced.
// Erase this warning with some do-nothing code.
#ifdef LSE_VISUALSTUDIO
static_cast<LSUINT32>(_tIndex);
#endif // #ifdef LSE_VISUALSTUDIO
m_ptData[_tIndex].~_tType();
}


The array is stored as T * as was mentioned above, so I construct and destruct objects via that index.


The first function that would be called is the allocator:
/**
* Allocate a given number of elements.
* If the allocation is less than what there already is, items are removed.
*
* \param _ui32Total Number of elements to allocate.
* \return Returns true if there was enough memory to allocate the given amount of
* objects. If _ui32Total is 0, true is always returned.
*/
LSBOOL LSE_CALL Allocate( LSUINT32 _ui32Total ) {
// If allocating 0 bytes, just reset the list.
if ( !_ui32Total ) {
Reset();
return true;
}
if ( Parent::m_tLen == _ui32Total ) { return; } // Nothing to do.
// Destroy items that are going to be removed.
if ( Parent::m_tLen ) {
for ( LSUINT32 I = Parent::m_tLen; --I >= _ui32Total; ) {
Parent::Destroy( I );
}
}
// Adjust the length.
if ( Parent::m_tLen > _ui32Total ) {
Parent::m_tLen = _ui32Total;
}
// Attempt to allocate.
_tType * ptNew = reinterpret_cast<_tType *>(m_paOurAllocator->Alloc( _ui32Total * sizeof( _tType ) ));
if ( !ptNew ) { return false; }
// Construct and copy all the items in the newly allocated array.
for ( LSUINT32 I = Parent::m_tLen; I--; ) {
// Construct new.
new( &ptNew ) _tType;
// Copy from old to new.
ptNew = Parent::m_ptData;
// Destroy old.
Parent::Destroy( I );
}
// Remove the old list.
if ( Parent::m_ptData ) {
m_paOurAllocator->Free( Parent::m_ptData );
}
// Success.
Parent::m_ptData = ptNew;
Parent::m_tAllocated = _ui32Total;
return true;
}


Special case #1: Handle allocating a size of 0 differently; it should just deallocate everything and free all memory. Otherwise you will attempt to allocate 0 bytes.
Here it is possible to allocate 6 objects when the list already contains more than 6 objects, so the first step is to destroy all the objects that are going to be deallocated.
Destroy them one-by-one from the end of the list.
Then I allocate with my own memory manager but you should use ::malloc(). Nothing in the new array is constructed yet so the next loop constructs the objects and copies from the old list to the new list via the = copy operator. Again one-by-one.
I am not handling exceptions here but you may want to do so.
Finally the old list is freed. You would use ::free().


The Reset() function frees all memory and sets the object back to its initial state. It can be called in the destructor for your vector-like class.
/**
* Reset the list completely.
*/
LSVOID LSE_CALL Reset() {
for ( LSUINT32 I = Parent::m_tLen; I--; ) {
Parent::Destroy( I );
}
if ( Parent::m_ptData ) {
m_paOurAllocator->Free( Parent::m_ptData );
Parent::m_ptData = NULL;
}
Parent::m_tLen = Parent::m_tAllocated = 0;
}

Very simple.


Inserting objects has a few special cases.
/**
* Insert an element.
*
* \param _tVal The item to insert.
* \param _ui32Index The index where the item is to be inserted.
* \return Returns false if memory could not be allocated. In this case, the list is not modified.
*/
LSBOOL LSE_CALL Insert( const _tType &_tVal, LSUINT32 _ui32Index ) {
assert( _ui32Index <= Parent::m_tLen );
// If inserting at the end, just push the item.
if ( _ui32Index == Parent::m_tLen ) { return this->Push( _tVal ); }

// Now we know we are inserting in the middle somewhere.
if ( Parent::m_tLen == Parent::m_tAllocated ) {
// Overflow checking.
_tDataType tNewTotal = Parent::m_tAllocated + _uAllocSize;
assert( tNewTotal > Parent::m_tAllocated );
if ( !Allocate( tNewTotal ) ) { return false; }
}

// Move other items.
// Since this is not a PoD handler, we cannot simply move memory.
// The last item has not been constructed yet, so we cannot call its copy operator yet.
this->Construct( Parent::m_tLen );
// Move items up one-by-one.
for ( LSUINT32 I = Parent::m_tLen; I > _ui32Index; --I ) {
Parent::m_ptData = Parent::m_ptData[I-1UL];
}
// No need to destroy/construct the item at the given location. Its copy operator should handle freeing of
// its memory.
Parent::m_ptData[_ui32Index] = _tVal;
++Parent::m_tLen;
return true;
}

First a few basic checks, and then a possible allocation if needed. Then the important parts.
Special case #2: The last item will soon have an object copied into it, so it is constructed. Be careful not to overlook this.
Then objects are copied over one-by-one.
The object at the insertion point has already been constructed, so calling its copy operator is enough.


Finally, removing items follows.
/**
* Remove elements without reallocating.
*
* \param _ui32Index The start index of the items to be removed.
* \param _ui32Total The number of items to remove.
*/
LSVOID LSE_CALL RemoveRangeNoDealloc( LSUINT32 _ui32Index, LSUINT32 _ui32Total ) {
if ( _ui32Total == 0UL ) { return; }
assert( _ui32Index < Parent::m_tLen );
assert( _ui32Index + _ui32Total <= Parent::m_tLen );
LSUINT32 ui32End = Parent::m_tLen - _ui32Total;

// Copy items over it.
// Since this is not a PoD handler, we cannot simply move memory.
LSUINT32 ui32CopyEnd = Parent::m_tLen - _ui32Total;
for ( LSUINT32 I = _ui32Index; I < ui32CopyEnd; ++I ) {
Parent::m_ptData = Parent::m_ptData[I+_ui32Total];
}

// Destroy the tail items that were moved down.
for ( LSINT32 I = Parent::m_tLen; --I >= ui32End; ) {
// Destruct the item.
Parent::Destroy( I );
}
Parent::m_tLen = Parent::m_tLen - _ui32Total;
}

Again we start with basic checks.
Then we move all the items down by whatever number is in _ui32Total using just the copy operator.
That leaves _ui32Total number of trailing duplicates at the end of the array, so a second loop destroys those items.
If you wanted to have a RemoveRange() function that resizes the list, it would just call this function internally and then call Allocate() with a new list size.


That is basically all there is too it. There are just a few places where you have to be careful to construct objects, and remember to keep the construction and destruction count the same for every object.
Once an object is constructed you can just use = operator to copy into it.
As was mentioned, you can’t just move memory, so you have to move each object one-by-one as shown here.

This should cover all the issues you will have with your own implementation.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid


Stuff


Why do you use operator = and not the copy constructor for copying the elements?

This topic is closed to new replies.

Advertisement