0

My question is summed up pretty well with this code snippet:

struct T {
    int* heapValue;
    T(){
        this->heapValue = new int[3]; // any dynamic size
    }
    ~T(){
        delete[] this->heapValue;
    }
}

int main() {
    int N = 5; // some value determined at runtime
    T* array = new T[N];

    for (int i = 0; i < N; i++)
    {
      T t = T(123, 456, args);
      t.do_something();

      array[i] = t;
    } // uh oh, t is deconstructed! and array[i].heapValue is invalidated

    int x = array[0].heapValue[0]; // segfault
}

I was able to solve this by creating T** array instead and making each t with new T(args), but then when it comes time to deallocate array I need to loop through each element and delete it first before deleting the array itself. This is slow and inconvenient. Also, T** looks like a 2-dimensional array which is potentially misleading.

The same issue occurs with std::vector<T> and can be solved with the equally slow and inconvenient std::vector<T*>.

Is there any way to create a dynamic array T* such that the elements are not deconstructed right after being assigned?

Muzammil Ismail
  • 303
  • 4
  • 9
Gaberocksall
  • 359
  • 2
  • 13
  • Don't use plain pointers, use smart pointers. – fredrik Oct 28 '21 at 05:29
  • 2 people have told me that this is impossible, but I'm having a hard time believing it because it seems like it would be pretty common to create an array of a custom class... – Gaberocksall Oct 28 '21 at 05:30
  • @fredrik I hear you, but I'm not exactly sure how that solves the problem at hand. It would still have to be an array of smart pointers, which would point to objects elsewhere on the heap, no? – Gaberocksall Oct 28 '21 at 05:31
  • 6
    The primary issue here is you're violating of the [Rule of Three](https://stackoverflow.com/questions/4172722/what-is-the-rule-of-three). You need to define a copy constructor for `T` so that you don't break encapsulation on assignment. It would then be more efficient to define a move-constructor and use that instead, to avoid extra allocations in your loop. – paddy Oct 28 '21 at 05:34
  • You may have to define a copy constructor/assignment operator for `T`. It's not a type that was trivially constructible/destructible, therefore it's also not a type that is trivially copyable. – Tharsalys Oct 28 '21 at 05:35
  • [The rule of three/five/zero - cppreference.com](https://en.cppreference.com/w/cpp/language/rule_of_three) – David C. Rankin Oct 28 '21 at 05:35
  • With smart pointers the object would not be deconstructed until the last smart pointer was destroyed. The smart pointer would be copied, not your object. – fredrik Oct 28 '21 at 05:38
  • Why didn't you make your example self contained? `T` doesn't have a constructor that takes three arguments. There's no sign of anything called `args`. Also, the negative thing you're experiencing happens with just `T a, b; b = a;` – Wyck Oct 28 '21 at 05:44
  • 2
    Smart pointers are not a drop-in solution to improper class design in the first place. While smart pointers are generally useful for both correctness and brevity, they are not always available (if you are tied to older compilers) and not always properly used (_e.g._ prolific use of `shared_ptr` when `unique_ptr` is required). And they can even lead to muddying a program's ownership semantics and even impacting performance. Proper use of smart pointers is almost as difficult as proper use of raw pointers. It's important to have experience and knowledge of both. – paddy Oct 28 '21 at 05:47
  • @paddy It's a pity there aren't many good resources that explain ownership semantics in-depth. ISO C++ core guidelines mention the concept but do not explain sufficiently. Do you know any? – Tharsalys Oct 28 '21 at 05:55
  • maybe std::vector> array; – che.wang Oct 28 '21 at 06:06

3 Answers3

-1

The comments under your questions already summed up all the possibilities to handle this problem. Anyway, here is quick example how to use move semantics, with minimal changes to your code:

struct T {
    int* heapValue;
    T(){
        std::cout << "CTOR: " << this << std::endl;
        this->heapValue = new int[3]; // any dynamic size
    }
    ~T(){
        std::cout << "DTOR: " << this << std::endl;
        delete[] this->heapValue;
    }
    T(const T& other) = delete;
    T& operator=(const T& other) = delete;
    T& operator=(T&& other)
    {  // this can be more simple if you decide to use std::unique_ptr to hold your heapValue
        delete[] this->heapValue;
        this->heapValue = other.heapValue;
        other.heapValue = nullptr;
        return *this;
    }
    T(T&& other)
    {
        *this = std::move(other);  // to avoid duplicate code
    }
    void do_something(){
        std::cout << "do_something: " << this << std::endl;
        heapValue[0]= 123;  // just for demonstration
        heapValue[1]= 456;
        heapValue[2]= 789;
    }
};

int main() {
    int N = 5; // some value determined at runtime
    T* array = new T[N];

    for (int i = 0; i < N; i++)
    {
      T t = T();
      t.do_something();
      array[i] = std::move(t);
    }
    std::cout << array[0].heapValue[0] << std::endl;
    std::cout << array[0].heapValue[1] << std::endl;
    std::cout << array[0].heapValue[2] << std::endl;
}

By deleting copy constructor and assignment we make sure there will be no invalid memory access. By providing move assignment we have a cheap way of storing your objects in the array.

pptaszni
  • 5,591
  • 5
  • 27
  • 43
-1

Is there any way to create a dynamic array T* such that the elements are not deconstructed right after being assigned?

Yes, but it's far less efficient than just using T** or std::vector<T*> though.

Basically, I needed to add a copy constructor T(const T& other) so that a second T could be created on the heap and copied from the stack instance, then the stack instance could safely be deconstructed. This document describes the rule of three that some commenters were talking about.

However, this is quite inefficient because two instances must be constructed (and one destructed), which is not ideal.

In conclusion, it's likely better to just use an array of pointers like so:

T** arr = new T*[size];
for (int i = 0; i < size; i++)
{
    arr[i] = new T(args);
}

Of course you can also use a std::vector<std::unique_ptr<T>> or some other combination, but the idea is to allocate the T instances on the heap from the get-go.

Gaberocksall
  • 359
  • 2
  • 13
-4

In this line

T t = T(123, 456, args);

you are creating a local variable, which quickly goes out of scope, so the pointer you are trying to use is invalidated, try allocating in loop with new

ygroeg
  • 143
  • 9
  • I'm aware. Please read the bottom text of the post. – Gaberocksall Oct 28 '21 at 05:32
  • Bottom text of post is "Is there any way to create a dynamic array T* such that the elements are not deconstructed right after being assigned?" try allocating in loop with new for values of array. Local variable going out of scope is the reason you get segfault – ygroeg Oct 28 '21 at 05:38
  • Well, I read the same bottom of the post that you did, and to my eyes your answer does not solve that. It simply is not possible because you're adding an extra layer of indirection that was to be specifically avoided. Feel free to dig your heels in and split hairs, but this answer is not useful and does not address the fundamental question being asked. It also demonstrates you didn't read the rest of the question where they _did_ try the thing you are suggesting. – paddy Oct 28 '21 at 05:56
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Oct 28 '21 at 06:50