Skip to main content
gamedev.net
🔒 Locked

Manual construction and destruction

Started by Servant of the Lord 35 replies 7.1k views
Original Post
Servant of the Lord
Servant of the Lord
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?

SiCrane
SiCrane

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.
Hodgman
Hodgman
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].
Servant of the Lord
Servant of the Lord
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.
Trienco
Trienco

//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
rip-off
rip-off
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.
SiCrane
SiCrane

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.
Bregma
Bregma

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
L. Spiro
L. Spiro
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
patrrr
patrrr

Stuff


Why do you use operator = and not the copy constructor for copying the elements?
L. Spiro
L. Spiro
Because it isn’t necessarily defined.


[EDIT]
Actually I tested this out about 5 years ago when I was not so advanced. I think I either made a mistake in my testing or I misinterpreted another error as being caused by copy-constructor.

Today, I see no reason it should not work. I am going to redo my tests.
[/EDIT]


[EDIT2]
It does work. I don’t know what my problem was in the past.
When possible, copy-constructors should be used as they reduce construction to a single stage instead of construct-then-copy (though poor implementations of copy constructors may yield the exact same result).

The new code inside of Allocate() is this:

// Construct and copy all the items in the newly allocated array.
for ( LSUINT32 I = Parent::m_tLen; I--; ) {
// Construct new.
new( &ptNew ) _tType( Parent::m_ptData );
// Destroy old.
Parent::Destroy( static_cast<_tDataType>(I) );
}

You could also use copy-constructor inside Insert() for the last element but you will have to rewrite the following array. I leave that up to you.
[/EDIT2]


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
SiCrane
SiCrane
Note that L. Spiro's code is not exception safe, so if you use exceptions in your code, you may not want to copy it directly.
L. Spiro
L. Spiro
I mentioned that in the post but it was a bit hidden under a small wall of text.
I have underlined it for clarity.



Why unsigned char* over char*?

When working with strings, I use [color=#0000ff]char. With data, [color=#0000ff]unsigned char. The reason is for safety while using the >> operator.
With [color=#0000ff]char, it will carry the sign bit, which is rarely desirable when working with raw binary data.


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
Servant of the Lord
Servant of the Lord
Man, the collective wisdom of a community like this really is a great resource - I'd hate to be trying to learn C++ pre-internet days. I really appreciate the time you have all taken to respond to my questions - especially on nitty-gritty C++ details like this, where I could easily burn myself and only realize it months later. Thank you.


[quote name='Servant of the Lord' timestamp='1351740412' post='4996060']
Why unsigned char* over char*?

When working with strings, I use [color=#0000ff]char. With data, [color=#0000ff]unsigned char. The reason is for safety while using the >> operator.
With [color=#0000ff]char, it will carry the sign bit, which is rarely desirable when working with raw binary data.[/quote]
What effect would that have? Would >> only shift 7 bits and ignore the sign bit?

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.[/quote]
Would it be better to use std::move() and the move constructor (since the compiler I use has access to it)?


[quote name='Servant of the Lord' timestamp='1351740412' post='4996060']
How am I supposed to free the raw memory? Just delete[]?

If you allocate with ::operator new(), you have to deallocate with ::operator delete().
[/quote]
How's that work? ::operator new() is saying "use the 'operator new' in global scope". Do classes have implicit operator new()s defined, and the implicit MyClass::operator new() then calls the class constructor? And the implicitly defined MyClass::operator delete() calls the class destructor?

I get that new() constructs the class, but what scope is 'new' in, if ::operator new() does not construct the class?

[hr]
I think it's coming together in my head. When I finish writing the code tomorrow, I'll post it and would like a few eyes to look over it if possible to correct any misunderstandings I might have.
Hodgman
Hodgman

What effect would that have? Would >> only shift 7 bits and ignore the sign bit?
There's two kinds of right shift -- arithmetic and logical. The former fills in the gap created at the top by "sign extending" (basically copying the top bit), and the later fills the gap with 0's.
IIRC, in C++ it's implementation-defined as to which kind of shift will be performed -- someone correct me if I'm wrong, or if C++11 has defined this behaviour!
In my experience every compiler has chosen the same implementation-defined behaviours: shifting a signed integer has always been an arithmetic (sign extending) shift, while shifting an unsigned integer is a logical shift.
L. Spiro
L. Spiro
You are correct about the standard. Not defined, but so common I have never seen it done any other way on any compiler.


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
iMalc
iMalc
I'm not at all clear how what you want is any different to a vector at all. Could you elaborate a little on any difference?

For example: I have written a container somewhat like a vector before, except that the memory is not contiguous, push_back is O(1) instead of amortised O(1), and random indexing is instead amortised O(1). There is no internal copy when growing, and each growth allocates an additional block twice the previous size. Searching starts from the largest block and proceeds towards the smaller blocks. The container provides no erase or insert methods. The iterator invalidation rules are relaxed compared to a vector.

You also said that you find yourself needing it, so I'm interested to hear what it is that you need which a vector does not provide. I'm half just wondering, half thinking that you might be mistaken in thinking that a vector isn't suitable.
Servant of the Lord
Servant of the Lord
I said that I find myself needing it, "more out of convenience and code cleanliness than necessity", and I was very careful to phrase it that way. smile.png

For convenience, because I have multiple classes needing it, I'm making a container that is very much like a vector.
It's a resizable 2D array... that resizes in all four directions (north, east, south, west).

I could just use a 1D vector, and index into it like ((y * width) + x) like I normally do and have done in the past.
Or I could use std::vector< std::vector<Type> > (which I don't particularly like, preferring 1D vectors treated as 2D to ensure the rows all stay the same lengths).

However, using a regular vector requires me to worry about re-arranging the location of the elements anyway.
Example: 3 x 3 grid
0 1 2
3 4 5
6 7 8

Resized to 5 x 5 grid:
0 1 2 3 4
5 6 7 8 +
+ + + + +
+ + + + +
+ + + + +

The elements are no longer in their correct positions, relative to each other.

I'd prefer it to be resized as:
0 1 2 + +
3 4 5 + +
6 7 8 + +
+ + + + +
+ + + + +

Unless I need to extend the grid on the other sides (which I do need in my code):
+ 0 1 2 +
+ 3 4 5 +
+ 6 7 8 +
+ + + + +
+ + + + +

So I created a container class called Grid, which is "a resizable 2D array container, that resizes without harming the relative location of the elements held by the container, and supports negative indices, like a 2D Cartesian grid." (important characteristics underlined - only the first is covered by std::vector)

Yes, I could use std::vector to solve the same thing, but then I'd have to A) have the classes using the vector fumbling around with keeping elements in-sync, B) always be offsetting their indices to correctly support negative indexing. The Grid class handles it all cleanly and intuitively, so I don't bloat the classes using Grid. The Grid class itself is less than 350 lines of code - including excessive whitespacing and comments - so Grid itself isn't bloated or untidy (I'm a real sucker for clean code wink.png).

So yes, "more out of convenience and code cleanliness than necessity".

I'll post it tomorrow, and you can judge it for yourself - I just want to fix one mistake I'm making somewhere in it where resizing is invalidating the data.

Really, the class only took me a few hours to put together - it's just the manual constructing/destructing parts that is new territory for me. And though I could've just made the class wrap std::vector internally (which would still require the class to be made anyway), it's worth taking the extra time to explore the new territory.
Bregma
Bregma

When working with strings, I use char. With data, unsigned char. The reason is for safety while using the >> operator.
With char, it will carry the sign bit, which is rarely desirable when working with raw binary data.

Minor correction: with [font=courier new,courier,monospace]char[/font] it may or may not carry the sign bit, depending on the implementation. With [font=courier new,courier,monospace]signed char[/font] or [font=courier new,courier,monospace]unsigned char[/font] you know what will happen.

Also, the effect of [font=courier new,courier,monospace]std::operator>>(std::ostream&, char*)[/font] is different from [font=courier new,courier,monospace]std::operator>>(ostream& unsigned char*)[/font].
Stephen M. Webb
Professional Free Software Developer
SiCrane
SiCrane

There's two kinds of right shift -- arithmetic and logical. The former fills in the gap created at the top by "sign extending" (basically copying the top bit), and the later fills the gap with 0's.
IIRC, in C++ it's implementation-defined as to which kind of shift will be performed -- someone correct me if I'm wrong, or if C++11 has defined this behaviour!

C++ defines shift on a unsigned integer type to be logical shift, however, the standard leaves room for shifting a signed integer to be different from either a logical or arithmetic shift. It would be standard compliant under C++11 for a shift, either left or right, on a signed integer to be a rotation/circular shift rather than either a logical or arithmetic shift. Actually, under C++11 a left shift on a negative signed integer is undefined behavior rather than implementation defined behavior. (Don't ask me to explain this one. To be pedantic a left shift on a signed integer with some non-negative values is also undefined behavior as well, though there's a proposed change to the standard sitting in the committee to fix that oddity.)

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.