Jump to content

  • Log In with Google      Sign In   
  • Create Account

Interested in a FREE copy of HTML5 game maker Construct 2?

We'll be giving away three Personal Edition licences in next Tuesday's GDNet Direct email newsletter!

Sign up from the right-hand sidebar on our homepage and read Tuesday's newsletter for details!


Manual construction and destruction


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
36 replies to this topic

#1 Servant of the Lord   Crossbones+   -  Reputation: 20247

Like
4Likes
Like

Posted 31 October 2012 - 09:26 PM

For the first time in 7 years of C++ programming, I think I need to use explicit constructor and destructor calls. Posted Image

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?

 

Any other tips or traps to watch out for?
It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.
All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.
Of Stranger Flames - [indie turn-based rpg set in a para-historical French colony] | Indie RPG development journal

[Fly with me on Twitter] [Google+] [My broken website]

[Need web hosting? I personally like A Small Orange]


Sponsor:

#2 SiCrane   Moderators   -  Reputation: 9625

Like
10Likes
Like

Posted 31 October 2012 - 10:45 PM

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()?

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*?

void * is more common, but a char pointer can be more convenient if you want to do pointer arithmetic.

Why unsigned char* over char*?

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?

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);

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?

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();

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?

Yes.

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

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?

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?

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.

#3 Hodgman   Moderators   -  Reputation: 30885

Like
5Likes
Like

Posted 31 October 2012 - 11:08 PM

Can you store it as a T* ? 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 alignof to help here.

There's also platform/OS-specific options besides new/malloc, such as _aligned_malloc.

Edited by Hodgman, 01 November 2012 - 03:49 AM.


#4 Servant of the Lord   Crossbones+   -  Reputation: 20247

Like
0Likes
Like

Posted 31 October 2012 - 11:10 PM

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.

Edited by Servant of the Lord, 31 October 2012 - 11:12 PM.

It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.
All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.
Of Stranger Flames - [indie turn-based rpg set in a para-historical French colony] | Indie RPG development journal

[Fly with me on Twitter] [Google+] [My broken website]

[Need web hosting? I personally like A Small Orange]


#5 Trienco   Crossbones+   -  Reputation: 2207

Like
3Likes
Like

Posted 31 October 2012 - 11:55 PM

//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

#6 rip-off   Moderators   -  Reputation: 8514

Like
5Likes
Like

Posted 01 November 2012 - 03:34 AM

You cannot use std::memcpy() unless the data type is "Plain old data".

How would "delete memoryA" know how much memory to delete?

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.

#7 SiCrane   Moderators   -  Reputation: 9625

Like
2Likes
Like

Posted 01 November 2012 - 07:32 AM

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

#8 Bregma   Crossbones+   -  Reputation: 5242

Like
1Likes
Like

Posted 01 November 2012 - 08:30 AM

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

If you allocate with ::operator new(), you have to deallocate with ::operator delete().
Stephen M. Webb
Professional Free Software Developer

#9 L. Spiro   Crossbones+   -  Reputation: 13945

Like
1Likes
Like

Posted 01 November 2012 - 09:18 AM

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[I] ) _tType;
				// Copy from old to new.
				ptNew[I] = Parent::m_ptData[I];
				// 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[I] = 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[I] = 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

Edited by L. Spiro, 01 November 2012 - 04:07 PM.

It is amazing how often people try to be unique, and yet they are always trying to make others be like them. - L. Spiro 2011
I spent most of my life learning the courage it takes to go out and get what I want. Now that I have it, I am not sure exactly what it is that I want. - L. Spiro 2013
I went to my local Subway once to find some guy yelling at the staff. When someone finally came to take my order and asked, “May I help you?”, I replied, “Yeah, I’ll have one asshole to go.”
L. Spiro Engine: http://lspiroengine.com
L. Spiro Engine Forums: http://lspiroengine.com/forums

#10 patrrr   Members   -  Reputation: 1027

Like
0Likes
Like

Posted 01 November 2012 - 09:50 AM

Stuff


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

#11 L. Spiro   Crossbones+   -  Reputation: 13945

Like
1Likes
Like

Posted 01 November 2012 - 10:14 AM

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[I] ) _tType( Parent::m_ptData[I] );
				// 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

Edited by L. Spiro, 01 November 2012 - 04:08 PM.

It is amazing how often people try to be unique, and yet they are always trying to make others be like them. - L. Spiro 2011
I spent most of my life learning the courage it takes to go out and get what I want. Now that I have it, I am not sure exactly what it is that I want. - L. Spiro 2013
I went to my local Subway once to find some guy yelling at the staff. When someone finally came to take my order and asked, “May I help you?”, I replied, “Yeah, I’ll have one asshole to go.”
L. Spiro Engine: http://lspiroengine.com
L. Spiro Engine Forums: http://lspiroengine.com/forums

#12 SiCrane   Moderators   -  Reputation: 9625

Like
0Likes
Like

Posted 01 November 2012 - 11:53 AM

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.

#13 L. Spiro   Crossbones+   -  Reputation: 13945

Like
1Likes
Like

Posted 01 November 2012 - 04:06 PM

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


L. Spiro

Edited by L. Spiro, 01 November 2012 - 04:12 PM.

It is amazing how often people try to be unique, and yet they are always trying to make others be like them. - L. Spiro 2011
I spent most of my life learning the courage it takes to go out and get what I want. Now that I have it, I am not sure exactly what it is that I want. - L. Spiro 2013
I went to my local Subway once to find some guy yelling at the staff. When someone finally came to take my order and asked, “May I help you?”, I replied, “Yeah, I’ll have one asshole to go.”
L. Spiro Engine: http://lspiroengine.com
L. Spiro Engine Forums: http://lspiroengine.com/forums

#14 Servant of the Lord   Crossbones+   -  Reputation: 20247

Like
0Likes
Like

Posted 01 November 2012 - 09:51 PM

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.


Why unsigned char* over char*?

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.

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.

Would it be better to use std::move() and the move constructor (since the compiler I use has access to it)?


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

If you allocate with ::operator new(), you have to deallocate with ::operator delete().

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?

 
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.
It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.
All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.
Of Stranger Flames - [indie turn-based rpg set in a para-historical French colony] | Indie RPG development journal

[Fly with me on Twitter] [Google+] [My broken website]

[Need web hosting? I personally like A Small Orange]


#15 Hodgman   Moderators   -  Reputation: 30885

Like
1Likes
Like

Posted 01 November 2012 - 10:16 PM

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.

Edited by Hodgman, 01 November 2012 - 10:19 PM.


#16 L. Spiro   Crossbones+   -  Reputation: 13945

Like
0Likes
Like

Posted 01 November 2012 - 11:25 PM

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
It is amazing how often people try to be unique, and yet they are always trying to make others be like them. - L. Spiro 2011
I spent most of my life learning the courage it takes to go out and get what I want. Now that I have it, I am not sure exactly what it is that I want. - L. Spiro 2013
I went to my local Subway once to find some guy yelling at the staff. When someone finally came to take my order and asked, “May I help you?”, I replied, “Yeah, I’ll have one asshole to go.”
L. Spiro Engine: http://lspiroengine.com
L. Spiro Engine Forums: http://lspiroengine.com/forums

#17 iMalc   Crossbones+   -  Reputation: 2313

Like
1Likes
Like

Posted 02 November 2012 - 12:36 AM

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.

Edited by iMalc, 02 November 2012 - 12:38 AM.

"In order to understand recursion, you must first understand recursion."
My website dedicated to sorting algorithms

#18 Servant of the Lord   Crossbones+   -  Reputation: 20247

Like
0Likes
Like

Posted 02 November 2012 - 01:47 AM

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. Posted Image

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 Posted Image).

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.

Edited by Servant of the Lord, 02 November 2012 - 01:49 AM.

It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.
All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.
Of Stranger Flames - [indie turn-based rpg set in a para-historical French colony] | Indie RPG development journal

[Fly with me on Twitter] [Google+] [My broken website]

[Need web hosting? I personally like A Small Orange]


#19 Bregma   Crossbones+   -  Reputation: 5242

Like
1Likes
Like

Posted 02 November 2012 - 02:42 AM

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 char it may or may not carry the sign bit, depending on the implementation. With signed char or unsigned char you know what will happen.

Also, the effect of std::operator>>(std::ostream&, char*) is different from std::operator>>(ostream& unsigned char*).
Stephen M. Webb
Professional Free Software Developer

#20 SiCrane   Moderators   -  Reputation: 9625

Like
2Likes
Like

Posted 02 November 2012 - 05:18 AM

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




Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS