Search code examples
c++multidimensional-arraynew-operator

how to create a contiguous 2d array in c++?


I want to create a function that returns a contiguous 2D array in C++.

It is not a problem to create the array using the command:

 int (*v)[cols] = new (int[rows][cols]);

However, I am not sure how to return this array as a general type for a function. The function is:

  NOT_SURE_WHAT_TYPE create_array(int rows, int cols)
  {
        int (*v)[cols] = new (int[rows][cols]);
        return v;
  }

I tried double*[] and double** and both don't work. I wouldn't want to use double*, since I want to access this array from outside as a 2D array.

Related question: How do I declare a 2d array in C++ using new?


Solution

  • If you want to create an array where the data is contiguous and you don't want a 1-dimensional array (i.e. you want to use the [][] syntax), then the following should work. It creates an array of pointers, and each pointer points to a position into a pool of memory.

    #include <iostream>
    #include <exception>
    
    template <typename T>
    T** create2DArray(unsigned nrows, unsigned ncols, const T& val = T())
    {
       if (nrows == 0)
            throw std::invalid_argument("number of rows is 0");
       if (ncols == 0)
            throw std::invalid_argument("number of columns is 0");
       T** ptr = nullptr;
       T* pool = nullptr;
       try
       {
           ptr = new T*[nrows];  // allocate pointers (can throw here)
           pool = new T[nrows*ncols];  // allocate pool (can throw here)
           T* startpool = pool;  // Remember the start of the pool
     
           // now point the row pointers to the appropriate positions in
           // the memory pool
           for (unsigned i = 0; i < nrows; ++i, pool += ncols )
               ptr[i] = pool;
    
           // Initialize pool
           std::fill(startpool, startpool + nrows*ncols, val);
           return ptr;
       }
       catch (std::bad_alloc& ex)
       {
           delete [] ptr; // either this is nullptr or it was allocated
           throw ex;  // memory allocation error
       }
    }
    
    template <typename T>
    void delete2DArray(T** arr)
    {
       delete [] arr[0];  // remove the pool
       delete [] arr;     // remove the pointers
    }
    
    int main()
    {
       try 
       { 
          double **dPtr = create2DArray<double>(10,10);
          dPtr[0][0] = 10;  // for example
          delete2DArray(dPtr);  // free the memory
       }
       catch(std::bad_alloc& ex)
       {
          std::cout << "Could not allocate array";
       }
    }
    

    Note that only 2 allocations are done. Not only is this more efficient due to the lesser amounts of allocations done, we now have a better chance of doing a rollback of the allocated memory if a memory allocation fails, unlike the "traditional" way of allocating a 2D array in non-contiguous memory:

    // The "traditional" non-contiguous allocation of a 2D array (assume N x M)
    T** ptr;
    ptr = new T*[N];
    for (int i = 0; i < N; ++i)
       ptr[i] = new T [M]; // <<-- What happens if new[] throws at some iteration?
    

    If new[] throws an exception somewhere during the operation of the for loop, you have to roll back all of the successful calls to new[] that happened previously -- that requires more code and adds complexity.

    Note how you deallocate the memory in the contiguous version -- just two calls to delete[] when allocated contiguously instead of a loop calling delete[] for each row.


    Also, since the data is in contiguous memory, algorithms, functions, etc. that assume that the data is in contiguous memory, just like a one-dimensional array, can now be used by specifying the start and end range for the M*N matrix:

    [&array[0][0], &array[M-1][N])

    For example:

    std::sort(&myArray[0][0], &myArray[M-1][N]);

    will sort the entire matrix in ascending order, starting from index [0][0] up until the last index [M-1][N-1].

    You can improve on the design by making this a true class instead of having allocation / deallocation as 2 separate functions.


    Edit: The class is not RAII-like, just as the comment says. I leave that as an exercise for the reader. One thing missing from the code above is the check that nRows and nCols are > 0 when creating such an array.

    Edit 2: Added a try-catch to ensure a proper roll back of the memory allocation is done if a std::bad_alloc exception is thrown attempting to allocate memory.


    Edit: For a 3 dimensional array example of code similar to the above see this answer. Included is code to roll back allocations if the allocation fails.


    Edit: Rudimentary RAII class added:

    #include <exception>
    #include <iostream>
    
    template <typename T>
    class Array2D
    {
        T** data_ptr;
        unsigned m_rows;
        unsigned m_cols;
    
        T** create2DArray(unsigned nrows, unsigned ncols, const T& val = T())
        {
            T** ptr = nullptr;
            T* pool = nullptr;
            try
            {
                ptr = new T*[nrows];  // allocate pointers (can throw here)
                pool = new T[nrows*ncols];  // allocate pool (can throw here)
                T* startpool = pool;  // Remember the start of the pool
    
                // now point the row pointers to the appropriate positions in
                // the memory pool
                for (unsigned i = 0; i < nrows; ++i, pool += ncols )
                    ptr[i] = pool;
    
                // Initialize pool
                std::fill(startpool, startpool + nrows*ncols, val);       
                return ptr;     
            }
            catch (std::bad_alloc& ex)
            {
                delete[] ptr; // either this is nullptr or it was allocated
                throw ex;  // memory allocation error
            }
        }
    
        void deleteAllData() noexcept
        {
            if (data_ptr)
            {
                delete[] data_ptr[0];  // remove the pool
                delete[] data_ptr;     // remove the pointers
                data_ptr = nullptr;  
            }
        }
    
        public:
            typedef T value_type;
            T** data() {
                return data_ptr;
            }
    
            unsigned get_rows() const {
                return m_rows;
            }
    
            unsigned get_cols() const {
                return m_cols;
            }
    
            Array2D() : data_ptr(nullptr), m_rows(0), m_cols(0) {}
            Array2D(unsigned rows, unsigned cols, const T& val = T())
            {
                if (rows == 0)
                    throw std::invalid_argument("number of rows is 0");
                if (cols == 0)
                    throw std::invalid_argument("number of columns is 0");
                data_ptr = create2DArray(rows, cols, val);
                m_rows = rows;
                m_cols = cols;
            }
    
            ~Array2D()
            {
                deleteAllData();
            }
    
            Array2D(const Array2D& rhs) : m_rows(rhs.m_rows), m_cols(rhs.m_cols)
            {
                data_ptr = create2DArray(m_rows, m_cols);
                std::copy(&rhs.data_ptr[0][0], &rhs.data_ptr[m_rows-1][m_cols], &data_ptr[0][0]);
            }
    
            Array2D(Array2D&& rhs) noexcept
            {
                data_ptr = rhs.data_ptr;
                m_rows = rhs.m_rows;
                m_cols = rhs.m_cols;
                rhs.data_ptr = nullptr;
            }
    
            Array2D& operator=(Array2D&& rhs) noexcept
            {
                if (&rhs != this)
                {
                    deleteAllData();
                    swap(rhs, *this);
                }
                return *this;
            }
    
            void swap(Array2D& left, Array2D& right) noexcept
            {
                std::swap(left.data_ptr, right.data_ptr);
                std::swap(left.m_cols, right.m_cols);
                std::swap(left.m_rows, right.m_rows);
            }
    
            Array2D& operator = (Array2D rhs)
            {
                swap(*this, rhs);
                return *this;
            }
    
            T* operator[](unsigned row)
            {
                return data_ptr[row];
            }
    
            const T* operator[](unsigned row) const
            {
                return data_ptr[row];
            }
    
            void create(unsigned rows, unsigned cols, const T& val = T())
            {
                *this = Array2D(rows, cols, val);
            }
    };
    
    int main()
    {
        try
        {
            Array2D<double> dPtr(10, 10);
            std::cout << dPtr[0][0] << " " << dPtr[1][1] << "\n";
        }
        catch (std::exception& ex)
        {
            std::cout << ex.what();
        }
    }