0

I've searched for similar questions but I couldn't find any satisfactory for my needs.

I'm a Computer Science student currently studying Algorithms and Data Structures. For my exam, I had to implement a collection of templatized data structures in C++. I was not allowed to use the STL as this was an exam question as to how to implement a library similar to the STL.

My implementation works however I would like to ask you for an advice about dynamic memory allocation.

Some of these data structures use a dynamic array (actually a raw pointer) to store elements, which automatically grows when full and shrinks under a certain load factor threshold (doubling and halving its size, respectively). For the sake of simplicity (and also because I'm not supposed to use them), I didn't use any of "modern stuff" such as smart pointers or move constructor/operator=, and basically I relied on C++98 features. I used new [ ] and delete [ ], but I read everywhere that it is a bad practice.

My question is: what is the proper way to handle dynamic memory allocation for array-based data structures in C++?

Here's an example of what I did (the array has been previously allocated by new [ ]):

template <typename T>
void ArrayList<T>::pushBack(T item) 
{
    if (size < capacity) {  // if there's room in the array
        array[size] = item; // simply add the new item
    } else { // otherwise allocate a bigger array                   
        capacity *= 2;
        T *temp = new T[capacity];
        // copy elements from the old array to the new one
        for (int i = 0; i < size; ++i)
            temp[i] = array[i];
        delete [] array;
        temp[size] = item;
        array = temp;
    }
    ++size;
}
Richard Chambers
  • 16,643
  • 4
  • 81
  • 106
Fabio Nardelli
  • 135
  • 2
  • 11
  • It is bad practice to use new and delete simply because most of the time you don't need to. Instead you should use containers from the STL that handles this for you. No need to re-invent the wheel. A `std::vector` will probably behave a bit similar to your code internally. – super Oct 26 '18 at 13:55
  • Your array is broken - when you allocate new capacity you call ctors for all elements, even ones that not suppose to exist yet. – Slava Oct 26 '18 at 14:00
  • @super I know, but studying Algorithms and Data Structures I'm actually required to reinvent the wheel, so I can't use `std::vector`! – Fabio Nardelli Oct 26 '18 at 14:03
  • @Slava I know that `new` both allocates memory and calls constructors, an this is another reason why I'm asking this question. Should I use `std::allocator`? – Fabio Nardelli Oct 26 '18 at 14:07
  • One issue with using `new` (or dynamically allocated memory) is forgetting to delete the memory, which leads to memory leaks. Another issue is copying; do you copy the entire object (deep copy) or do you copy the pointer (shallow copy); if you copy the pointer, you may have ownership issues (like when can the object be deleted). – Thomas Matthews Oct 26 '18 at 14:11
  • "Should I use std::allocator?" no you should use `std::vector`, problem is you cannot implement `std::vector` by yourself without hitting UB – Slava Oct 26 '18 at 14:12
  • @Slava I already explained that I can't use `std::vector` because it's for a homework about data structures. Could you please explain what hitting UB means? English is not my mother tongue – Fabio Nardelli Oct 26 '18 at 14:21
  • @Thomas Matthews I followed the rule of 3 so I made a copy constructor also. – Fabio Nardelli Oct 26 '18 at 14:22
  • It means you cannot legally properly implement `std::vector` functionality not being system library developer due to issues in current standard. – Slava Oct 26 '18 at 14:22
  • @Slava so what should I do? P.S: just for curiosity, could you please tell me what literally means hitting UB? I like learning new terms :-) – Fabio Nardelli Oct 26 '18 at 14:29
  • UB stands for Undefined Behavior details can be found [here](https://en.cppreference.com/w/cpp/language/ub) or many other places. – Slava Oct 26 '18 at 14:33
  • "so what should I do?" you implemented them and they "work"? Fine. You do not really need to optimize them, use properly designed containers. – Slava Oct 26 '18 at 14:34
  • @Slava I didn't get that UB stood for Undefined Behaviour, I thought it was some slang stuff. Anyway thanks for your help. – Fabio Nardelli Oct 26 '18 at 14:39

2 Answers2

1

No, you still don't need new and delete. The only reason to still use new in C++ is to perform aggregate initialization, which std::make_unique does not support, and you never need delete at all.

Your code sample then becomes:

template <typename T>
void ArrayList<T>::pushBack(T item) 
{
    if (size < capacity) {  // if there's room in the array
        array[size] = item; // simply add the new item
    } else { // otherwise allocate a bigger array                   
        capacity *= 2;
        auto temp = std::make_unique<T[]>(capacity);
        // copy elements from the old array to the new one
        for (int i = 0; i < size; ++i)
            temp[i] = array[i];
        temp[size] = item;
        array = std::move(temp);
    }
    ++size;
}

Which can also be factored down by swapping the two sections:

template <typename T>
void ArrayList<T>::pushBack(T item) 
{
    if (size >= capacity) {  // if there's no room in the array, reallocate                 
        capacity *= 2;
        auto temp = std::make_unique<T[]>(capacity);
        // copy elements from the old array to the new one
        for (int i = 0; i < size; ++i)
            temp[i] = array[i];
        temp[size] = item;
        array = std::move(temp);
    }

    array[size] = item; // simply add the new item
    ++size;
}

Further possible improvements: move the elements when reallocating instead of copying them, use a standard algorithm instead of the manual for loop.

Quentin
  • 62,093
  • 7
  • 131
  • 191
  • Thanks for your answer. Is this only available in c++ 11? Do I need also move constructor and operator=? – Fabio Nardelli Oct 26 '18 at 15:57
  • Not sure how pertinent this is but should a note that this requires the type `T` to support copy construction? Using pointers as in the original post bypasses that requirement. Not that I am advocating using pointers, it is just a difference. – Richard Chambers Oct 26 '18 at 16:01
  • @Richard I don't see where this requires a copy constructor, only a default constructor and a move assignment operator. – Quentin Oct 26 '18 at 23:28
  • @Fabio C++11, plus C++14 for `std::make_unique` (but you can easily implement a replacement in C++11). – Quentin Oct 26 '18 at 23:29
  • @Quentin, thank you again for your contribution. Could you briefly tell me how to convert my whole class implementation to this way of allocating memory? Do I need to use a `make_unique` also in every other place in my class where I used `new` so far? And, have I to write a move constructor and an operator=? – Fabio Nardelli Oct 27 '18 at 11:54
  • Finally, there's still one thing I'm considering: with this kind of memory management, allocation and objects construction are made together, just like with `new`, aren't they? This reduces performance, because when the array gets doubled, 2*size objects are constructed even if the caller application doesn't need them all. While, if I remember correctly what I read on a book, with `std::allocator` we can separate memory allocation and objects construction, and this is just what `std::vector` does. Anyway, I think it's overkill for my purposes, it's just for curiosity. – Fabio Nardelli Oct 27 '18 at 11:54
  • @FabioNardelli yes, `std::make_unique`'s intent is to repace `new` to return immediately return a smart pointer instead of a raw owning pointer. Since `std::unique_ptr` is movable, you can let the compiler generate your move constructor and assignment operator. And you're right abour constructing empty objects: `std::make_unique` is but a thin wrapper around `new` and does not deal with uninitialized memory. `std::allocator` does, but you could also allocate storage with aligned `char` arrays and use placmeent-`new` to construct objects. (cont.) – Quentin Oct 27 '18 at 22:22
  • @FabioNardelli (cont.) However, as Slava noted in the comments, there is a loophole in the standard which makes implementing `std::vector` yourself impossible without relying on undefined behaviour; it's a matter of array-indexing (through iterators or `operator[]`) into the contiguous segment of constructed objects, which strangely do *not* consitute an array because they weren't constructed all at once as such (via `T[]`). – Quentin Oct 27 '18 at 22:22
  • @Quentin Thanks again for your help! – Fabio Nardelli Oct 27 '18 at 22:32
  • @Quentin Hi, do you suggest that I use smart pointers also for linked lists and trees? Thanks in advance. P.S: for my ArrayList class, is it correct to use `reset` in the operator= overload function? – Fabio Nardelli Oct 28 '18 at 17:53
  • @FabioNardelli You can use `reset` or `operator =` equally to transfer ownership. And yes, you can use `unique_ptr` for pretty much any allocation, with the caveat that a list linked with `unique_ptr`s ends up with a recursive destructor, which could be problematic for very large lists. – Quentin Oct 29 '18 at 11:20
0

I believe, for this project, that it is indeed proper to use new and delete; my Data Structures teacher uses the very same style of memory allocation. Seemingly, the general reason that people disapprove of using allocated memory, is that it can be hard to manage properly. It's just important to remember to delete all memory that you are no longer using -- don't want to have any orphaned RAM on your hands!

Little Boy Blue
  • 311
  • 4
  • 17
  • are you sure about that? wrt ++i. May want to run a test or 2 – Mike Oct 26 '18 at 15:23
  • @Mike Yes, because arrays start at [0]. If he sets i equal to 0 and then increments it right away, then he'll start with temp[1] = array[1], passing over temp[0] = array[0]. – Little Boy Blue Oct 26 '18 at 15:26
  • @LittleBoyBlue I think you misunderstand what `++i` and `i++` does. In this case it is indeed correct to use `++i`. – super Oct 26 '18 at 15:31
  • 1
    About whcih `i++` are you talking? I only see the one in the `for` loop and there it does not matter if you write `for( ... ; ... ; i++)` or `for( ... ; ... ; ++i)` see [Post-increment and pre-increment within a 'for' loop produce same output](https://stackoverflow.com/questions/4706199) – t.niese Oct 26 '18 at 15:32
  • In a for loop the counter variable is incremented after the body of the loop has been executed (e.g. after all the instructions in it have been run) so you can use equally either pre-increment or post-increment. – Fabio Nardelli Oct 26 '18 at 15:33
  • 1
    Using a tool like [godbolt.org](https://godbolt.org), you can see that both result in the [exact same compile output](https://godbolt.org/z/U5dG9h) – t.niese Oct 26 '18 at 15:37