3

I was programming a dynamic array for my own use, that i wanted pre-set with zeros.

template <class T>
dynArr<T>::dynArr()
{
rawData = malloc(sizeof(T) * 20); //we allocate space for 20 elems
memset(this->rawData, 0, sizeof(T) * 20); //we zero it!
currentSize = 20;
dataPtr = static_cast<T*>(rawData); //we cast pointer to required datatype.
}

And this part works - iterating by loop with dereferencind the dataPtr works great. Zeros.

Yet, reallocation behaves (in my opinion) at least a bit strange. First you have to look at reallocation code:

template <class T>
void dynArr<T>::insert(const int index, const T& data)
{

    if (index < currentSize - 1)
    {
        dataPtr[index] = data; //we can just insert things, array is zero-d
    }

    else
    {
        //TODO we should increase size exponentially, not just to the element we want

        const size_t lastSize = currentSize; //store current size (before realloc). this is count not bytes.

        rawData = realloc(rawData, index + 1); //rawData points now to new location in the memory
        dataPtr = (T*)rawData;
        memset(dataPtr + lastSize - 1, 0, sizeof(T) * index - lastSize - 1); //we zero from ptr+last size to index

        dataPtr[index] = data;
        currentSize = index + 1;
    }

}

Simple, we realloc data up to index+1, and set yet-non-zeroed memory to 0.

As for a test, i first inserted 5 on position 5 on this array. Expected thing happened - 0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

Yet, inserting something else, like insert(30,30) gives me strange behavior:

0, 0, 0, 0, 0, 5, 0, -50331648, 16645629, 0, 523809160, 57600, 50928864, 50922840, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30,

What the hell, am i not understanding something here? shouldnt realloc take all the 20 previously set memory bytes into account? What sorcery is going on here.

shajduk
  • 43
  • 3

1 Answers1

4

Problem 1:

You are using the wrong size in the call to realloc. Change it to:

rawData = realloc(rawData, sizeof(T)*(index + 1)); 

If rawData is of type T*, prefer

rawData = realloc(rawData, sizeof(*rawData)*(index + 1)); 

Problem 2:

The last term of the following is not right.

memset(dataPtr + lastSize - 1, 0, sizeof(T) * index - lastSize - 1); 

You need to use:

memset(dataPtr + lastSize - 1, 0, sizeof(T) * (index - lastSize - 1));
                               //  ^^              ^^
                               // size      *  The number of objects 

Problem 3:

Assigning to dataPtr using

dataPtr[index] = data;

is a problem when memory is obtained using malloc or realloc. malloc family of functions return just raw memory. They don't initialize objects. Assigning to uninitialized objects is a problem for all non-POD types.

Problem 4:

If T is type with virtual member functions, using memset to zero out memory will most likely lead to problems.


Suggestion for fixing all the problems:

It will be much better to use new and delete since you are in C++ land.

template <class T>
dynArr<T>::dynArr()
{
   currentSize = 20;
   dataPtr = new T[currentSize];
   // Not sure why you need rawData
}

template <class T>
void dynArr<T>::insert(const int index, const T& data)
{
   if (index < currentSize - 1)
   {
      dataPtr[index] = data;
   }

   else
   {
      const size_t lastSize = currentSize;
      T* newData = new T[index+1];
      std::copy(dataPtr, dataPtr+lastSize, newData);
      delete [] dataPtr;
      dataPtr = newData;
      dataPtr[index] = data;
      currentSize = index + 1;
   }
}

Please note that the suggested change will work only if T is default constructible.

This will also take care of the problems 3 and 4 outlined above.

Baum mit Augen
  • 49,044
  • 25
  • 144
  • 182
R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • 1
    @Swift That is an interesting question. Under what circumstances would (perhaps a move) assignment be invalid for a zeroed object. – Captain Giraffe May 11 '18 at 19:29
  • 1
    @CaptainGiraffe if it's not standard layout\POD class. ALso this may cause is issue with compiler. Technically what's going on there is UB unless placement new was used. Latter would construct object in allocated meory..thta's how memory pools are working – Swift - Friday Pie May 11 '18 at 19:30
  • 1
    @Swift In the general case, absolutely. I'm thinking about specific compilers behaviour. – Captain Giraffe May 11 '18 at 19:32
  • @CaptainGiraffe I think I saw similar code on SO that resulted that after reading object, memory of fields was not initialized, with newer versions of g++ (C++14 and so). WHat if class had a assignment opertator and was relying on virtual methods? Essentially you need a trivially constructable\copyable objects, then you can memcpy them.Sideeffects would be lost for sure. – Swift - Friday Pie May 11 '18 at 19:34
  • 2
    @Swift It is perfectly legal as long as the object does not have *non-vacuous initialization*. If it has *non-vacuous initialization* then placement new is needed. – NathanOliver May 11 '18 at 19:39
  • @NathanOliver well yeah..not something one want to leave in template code where that class can be anything. And now I want upvote R Sahu twice :P – Swift - Friday Pie May 11 '18 at 19:43
  • @Swift True. Since `T` could be anything this code definitely isn't safe. – NathanOliver May 11 '18 at 19:45
  • @NathanOliver Was that *non-vacuous* stuff added in C++17? Because before that, you need placement new even for `int`. (And before the same old discussion is rehashed, I'm aware that it works on all compilers and that the committee probably did not intend to make it that way.) – Baum mit Augen May 11 '18 at 19:55
  • @BaummitAugen I'm pretty sure you have never needed placement new for an `int`. In the C++03 standard had *The lifetime of an object of type T begins when: storage with the proper alignment and size for type T is obtained, and if T is a class type and the constructor invoked to create the object is non-trivial (12.1), the constructor call has completed. the initialization is complete.* So you we okay back then. C++11 used the word *non-trivial* and C++14 is where *non-vacuous* was introduced. – NathanOliver May 11 '18 at 20:01
  • @NathanOliver That paragraph is not relevant as you need to have an object before you can talk about its lifetime, see the discussion [here](https://stackoverflow.com/a/40874245/3002139). I think they intended to fix that at some point, but I don't know if that happened yet. Edit: No, looks like [intro.object] was not changed in C++17 to address this. – Baum mit Augen May 11 '18 at 20:05
  • @Swift why is "new" operator necessary here? Am i just not accessing a previously allocated location in a correct spot (since pointer is casted to T type) with correct size(since data is &T)? – shajduk May 11 '18 at 20:10
  • 1
    @shajduk Casting a pointer does not create objects in the memory it points to. – Baum mit Augen May 11 '18 at 20:11
  • @BaummitAugen but i just want to... copy the object there? Or am i not getting something? (im kind of new to C++) – shajduk May 11 '18 at 20:12
  • @BaummitAugen Excellent point. I hadn't thought about that part of it. Looks like it is UB then in all cases. – NathanOliver May 11 '18 at 20:13
  • 1
    @shajduk It's a bit technical, even very good C++ folks get confused sometimes. But in simple terms: You don't assign to a memory location, you assign to an object. And before you can do that, the memory must contain an object, and `malloc` does not create any. Thus you'd need placement new. (Tbh, getting a container like `std::vector` right with all its corner cases is *really* hard, I'm not sure I could do that myself. It took Microsoft many years.) – Baum mit Augen May 11 '18 at 20:16
  • @BaummitAugen oh, this is helpful. So i need to create for example an int object, so i can assign integer value to it? Or does it work only on objects and not primitives, thats why my array sort-of-worked? (i think the latter since, well, int is not an object of any sort). – shajduk May 11 '18 at 20:20
  • 1
    @shajduk "Primitives" like `int` are objects, too. So yes, technically, you need to create an `int` object before you can assign to it. However, note the "technically". The need to do so is relatively unknown, it's likely that the standards committee did not introduce it on purpose and it works on all compilers I know of anyway. But for types that are not "trivially default constructible", like `std::string` for example, you certainly need to actually create an object one way or another. – Baum mit Augen May 11 '18 at 20:25
  • 1
    @shajduk (Just an aside: the distinction between "object" and "primitive" you are talking about would roughly be described by class types vs. built-in types, in case you want to look that up.) – Baum mit Augen May 11 '18 at 20:28
  • @BaummitAugen I will! This sounds interesting. – shajduk May 11 '18 at 20:34
  • @BaummitAugen dataPtr[index] = new T; dataPtr[index] = data; could you tell me if this is this correct way to do this, or am i missing something? – shajduk May 11 '18 at 20:42
  • 1
    @shajduk, you'll have to use `new (dataPtr+index) T; dataPtr[index] = data;` – R Sahu May 11 '18 at 20:45
  • @shajduk This does not compile. The answer presents a good way for your level; if you have further questions, better ask them as a new question. These comment threads are not for extended discussions, and we needlessly ping R Sahu with every single one. – Baum mit Augen May 11 '18 at 20:45