简体   繁体   中英

Dynamic array implementation using void pointers

I'd like to implement something like below:

struct MyArray {
    void* Elements;
    int Capacity;
    int ElementsCount;
    size_t ElementSize;

    //methods
    void AddElement(void* item);
    //...
};

void* Elements should be pointer to items of any type. Every element should have particular size ( ElementSize variable) and AddElement(void*) method should add item to existing array. The problem is that I can't do pointer arythmetic with my Array, I know I need to use casting every time I want to use it but I completely don't know how to do it. And I know template would be better solution for it, but in this case I'd like to practise with pointers :)

Thanks for help in advance.

To move the pointer around, you can do:

int* nextInt = reinterpret_cast<int*>(Elements) + 1;

This would point to the next int . You can use this technique to move around other types.

Note that this can lead to all kinds of trouble, because of different sizes of the elements.

Yes, you cannot do pointer arithmetic on void*, you will have to cast to char* to do the arithmetic eg something like:

void MyArray::AddElement( void * item )
{
    // verify that ElementsCount is not already Capacity and if so, reallocate or throw
    void * insertionPoint = static_cast<char *>(Elements) + (ElementSize * ElementsCount );
    memcpy( insertionPoint, item, ElementSize );
    ++ElementsCount;
}

Note that you need static_cast to cast from void* to char* and you do not need to explicity cast at all back to void* which is why I can assign it to insertionPoint.

I would pursue this way:

  1. Create your array specific iterator, like STL ones.
  2. Overload operators for your iterators

Iterators will benefit your array as you may have generic algorithms that iterate through your array without knowledge of storage type needed.

I don't see how templates and pointers are mutually exclusive, I think this is the sort of situation why people use them in the first place. With a template you give a type to your pointers and problem is sorted.

On the other hand, if you avoid templates completely, you will need the size of the type. Say, the way how CashCow handles the problem:

void MyArray::AddElement( void * item )
{
    auto insertionPoint = static_cast<char *>(Elements) + (ElementSize * ElementsCount );
    memcpy( insertionPoint, item, ElementSize );
    ++ElementsCount;
}

Yet, you're not finished with it. You need to make sure that you will never exceed the pre-allocated buffer. I would modify it this way:

void MyArray::AddElement( void * item )
{
  if ((Capacity + 1) < ElementSize * ElementsCount)
  {
    Capacity <<= 1; // Double the size of the buffer.
    auto newBlock = new char[Capacity];
    memcpy(Elements, newBlock, Capacity >> 1); // Copy the old data
    delete Elements;
    Elements = static_cast<void*>(newBlock);
  }

  auto insertionPoint = static_cast<char *>(Elements) + (ElementSize * ElementsCount );
  memcpy( insertionPoint, item, Elementize );
  ++ElementsCount;
}

Something like this. Of course this is still not complete but gives you perhaps a clue.

I have to question why you'd want to do this. Unless you are doing this for academic / experimental purposes and your planning to throw it away, you're making work for yourself and are almost certain to end up with code which is more vulnerable to problems than if you used facilities that the language and STL already offer. In C, you'd probably have to do this, but with C++ you don't have to as the language support is there.

There are two aspects to what you're doing: Having elements of any type that can be used generically in some defined way and collecting those elements up together. Then first aspect can easily be acheived by polymosphism. Create an abstract base class which defines a common interface:

struct BaseElement { virtual void doSomething( ); };

Then you can derive structs from this which cover what your elements are doing:

struct DerivedElement1 : public BaseElement { void doSomething( ); };

struct DerivedElement2 : public BaseElement { void doSomething( ); };

To collect the types together, you can simply use an STL vector. It provides all of what you need as far as I can see. As a very simple example, you could do the following:

// Convenient shorthand.
typedef std::vector< std::shared_ptr<BaseElement> > MyElements;
MyElements m;

// Create two different but commonly derived objects.
std::shared_ptr<DerivedElement1> e1(new DerivedElement1);
std::shared_ptr<DerivedElement2> e2(new DerivedElement2);

// Push them onto the collection.
m.push_back( e1.static_pointer_cast<BaseElement>( e1 ) );
m.push_back( e2.static_pointer_cast<BaseElement>( e2 ) );

At this point you've got everything you need. Vector provides standard functionality such as begin() , end() and size() which help you to traverse the collection and run STL algorithms on it if you so wish. The fact that the collection is polymorphic means that you can run doSomething() on each element knowing it will perform only what was defined for that struct.

(I've not got acccess to a C++11 compiler so I'm sure someone will pick me up on something here. However, the same thing can easily be acheived with pre C++11 code even using raw pointers if you're careful to clear down your objects properly.)

I know this isn't the answer you directly wanted, but I just wanted to emphasise that unless you're simply trying to learn with throw away examples, it's almost always faster, shorter, safer and more reliable to use what's already there.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM