0

I have simple wrapper class around C-style array. I don't want to use std::vector since I want to have only one array even if I do copy of the struct. With std::vector the vector is also copied.

struct RawDataArray {
    double* data;
    size_t size;
    
    static RawDataArray CreateNew(size_t size){
        return RawDataArray(new double[size], size);
    }

    static RawDataArray CreateNew(size_t size, double defaultValue){
       RawDataArray rd = RawDataArray::CreateNew(size);
       std::fill_n(rd.data, size, defaultValue);
       return rd;
    }

    static RawDataArray CreateCopy(double* data, size_t size) {
       RawDataArray rd = RawDataArray::CreateNew(size);
       std::copy(data, data + size, rd.data);
       return rd;
    } 

    RawDataArray(double* data, int size) :
        data(data),
        size(size)
    {}

    RawDataArray(const RawDataArray& other) :
        data(other.data),
        size(other.size)
    {}

    RawDataArray(RawDataArray&& other) noexcept :
        data(std::exchange(other.data, nullptr)),
        size(std::exchange(other.size, 0))
    {}

    RawDataArray& operator=(const RawDataArray& other){
        return *this = RawDataArray(other);
    }

    RawDataArray& operator=(RawDataArray&& other) noexcept {
        std::swap(data, other.data);
        std::swap(size, other.size);    
        return *this;
    }
}

However, the problem is memory management. If I shuffle the wrapper class around, I don't know if I can safely release data.

I was thinking of using std::shared_ptr<double[]>, which is available since C++17 for arrays. Is there any other way or idiom to use?

10
  • What actual problem are you trying to solve here? Apr 8, 2023 at 11:51
  • 2
    Why not shared_ptr<vector<double>> ? You could also wrap the vector in some other class and use a shared_ptr<MyWrapper>. Apr 8, 2023 at 12:28
  • @PhilipKendall To minimize memory allocations. If I use std::vector, it is copied with each copy of the wrapper object. So I was thinking of C++17 shared_ptr for the raw array. However, I am not sure if there is some "better" way Apr 8, 2023 at 12:29
  • @BenCottrell I was thinking of this as well, but it seems rather "weird" when I can create raw array directly and it is managed by shared_ptr as well. As for the wrapper object, I have there some other methods, it is not just a simple vector extension. Apr 8, 2023 at 12:30
  • @MartinPerry a raw array won't be managed by shared_ptr - you do not have any of the safety provided by the vector API when using raw arrays. If you have some other methods which are needed then a wrapper class seems like a sensible option - you can encapsulate whatever functionality is needed for working on the vector in the wrapper Apr 8, 2023 at 12:31

1 Answer 1

3

You're essentially writing your own smart pointer. Currently, your implementation leaks memory, since the array will never be deallocated. That's safe, but suboptimal. To know when the shared array can be safely freed, you'll have to track how many objects are sharing that array – you need a reference count. You increment the reference count in a copy constructor, and decrement it in the destructor. If the destructor sees the reference count drop to zero, the shared data is deleted.

By far, the easiest way to do that is to use a std::shared_ptr<double[]> instead of the raw pointer double*. Things will Just Work™, and you won't have to think about copy- and move-constructors or destructors (compare the rule of zero).

There can be valid reasons not to use a shared_ptr. For example, a shared_ptr must use atomic reference counting to be threadsafe, even if your code will never share the array across threads. That may be a bit slower, depending on platform. And a shared_ptr will typically require two allocations – one for the contol block with the reference count and deleter, one for the actual data. While a shared_ptr needs this in order to support efficient weak references and because of details of the C++ standard, your C++ language implementation might allow you to use a more compact representation by using a C-style flexible array member. Then approximately:

struct Inner {
  size_t refcount;
  size_t capacity;
  double data[];  // !!! not ISO C++ !!!
};

struct SharedDataArray {
  Inner* inner;

  SharedDataArray() : inner(nullptr) {}

  explicit SharedDataArray(size_t capacity) : SharedDataArray() {
    if (!capacity) return;

    inner = reinterpret_cast<Inner*>(std::malloc(
      sizeof(Inner) + capacity * sizeof(double)
    ));
    if (!inner) throw std::bad_alloc();
    inner->refcount = 1;
    inner->capacity = capacity;
    std::fill(inner->data, inner->data + capacity, 0);
  }

  SharedDataArray(SharedDataArray& other) : inner(other.inner) {
    if (!inner) return;

    ++(inner->refcount);
  }

  ~SharedDataArray() {
    if (!inner) return;

    --(inner->refcount);
    if (inner->refcount > 0) return;
    std::free(inner);
    inner = nullptr; // not strictly necessary
  }
};

But this is tricky to get right, and you should really consider using a shared_ptr instead. Preferably a shared_ptr<vector<double>> since that manages the array lifecycle for you, at the cost of an additional pointer indirection.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.