2

I want to implement a 2D mesh class which is memory efficient, since it will be used in a Monte Carlo simulation. At this stage however, this is actually the same as a 2D array class. Needless to say, I am new to C++. I have written the following code.

#define MESH2D_H_INCLUDED

class Mesh2D
{
private:

    double* mesh;
    size_t  rows;
    size_t  columns;

public:

    /* constructor */
    Mesh2D(size_t Nx, size_t Ny){
        rows    = Nx;
        columns = Ny;
        mesh    = new double[Nx*Ny] {};
        std::cout << "Mesh created." << std::endl;
    }

    /* destructor */
    ~Mesh2D()
    {
        delete[] mesh;
        std::cout << "Mesh deleted." << std::endl;
    }

    /* accessors */
    double getrows() const {return rows;}
    double getcolumns() const {return columns;}
    double& operator()(size_t i, size_t j)
    {
        if (i > rows || j > columns){
            throw std::out_of_range("Index exceeds array bounds.");
        }
        return mesh[j + i*columns]; // row major access
    }

    /* copy constructor */
    Mesh2D& operator=(const Mesh2D& rhs) // what is the difference between this line and the following? inline operator=(const Mesh2D& rhs)
    {
        if (rhs.rows != rows || rhs.columns != columns){
            throw std::out_of_range("Assignment cannot be performed, mesh dimensions do not agree.");
        }
        mesh = new double [rows*columns];

//        I want to avoid using a for loop
//        for (int i=0; i<rows*columns; i++){
//            mesh[i] = rhs.mesh[i];
//        }
//      Use this instead
        memcpy(mesh, rhs.mesh, sizeof(rhs)); //however the output is not the expected.
        std::cout << "copied!" << std::endl;
    }

};

#endif // MESH2D_H_INCLUDED
//MAIN FUNCTION

#include <iostream>
#include "Mesh2D.h"

void PrintMesh2D(Mesh2D &mesh){ //why isn't it going to work if I add const? I mean: void PrintMesh2D(const Mesh2D &mesh)

    for (int i=0; i<mesh.getrows(); i++){

        for (int j=0; j<mesh.getcolumns(); j++){

            std::cout << mesh(i,j) << " ";
        }
        std::cout << std::endl;
    }
}

int main()
{

    Mesh2D mesh{3,3};
    Mesh2D newmesh{3,3};

    for (int i=0; i<mesh.getrows(); i++){
        for (int j=0; j<mesh.getcolumns(); j++){
            mesh(i,j) = j + i * mesh.getcolumns();
        }
    }

    newmesh = mesh;
    PrintMesh2D(newmesh);

}

console output

My questions are written as comments but I am listing them here as well:

  1. Inside the copy constructor, what is the difference between this line Mesh2D& operator=(const Mesh2D& rhs) and this line inline operator=(const Mesh2D& rhs)?

  2. Again, inside the copy constructor I would like to avoid a for loop and use memcpy instead. However, the output is not the expected, what am I doing wrong?

  3. Inside the main function: Why is this not going to compile? void PrintMesh2D(const Mesh2D &mesh)

  4. Finally, is the implementation complete? I mean, are there any important features/functions that are missing?

You are welcome to give me any piece of advice. Since I am new to C++, I have the feeling that I am writing bad buggy code.

EDIT:

I wrote the following as a copy constructor.

/* copy constructor */
    Mesh2D (const Mesh2D& rhs)
    {
        rows    = rhs.rows;
        columns = rhs.columns;
        mesh    = new double[rows*columns];
        memcpy(mesh,rhs.mesh,rows*columns*sizeof(double));
        std::cout << "Copied!" << std::endl;
    } 

Looks right?

Angelos
  • 169
  • 10
  • *Inside the copy constructor,* -- Your `Mesh` class does not have a copy constructor. It has an assignment operator, thus it is missing the requisite functions for `Mesh` to be safely copyable -- *Since I am new to C++, I have the feeling that I am writing bad buggy code.* -- You could alleviate all of these issues if you used `std::vector mesh;` instead of `mesh *`. – PaulMcKenzie Jul 03 '20 at 07:31
  • Your test code is not a good test. This simple 2 line program causes issues: `int main() { Mesh2D mesh{3,3}; Mesh2D newmesh = mesh; }` -- That shows the issue because of the lack of a copy constructor. – PaulMcKenzie Jul 03 '20 at 07:44
  • 1
    You should read up on the [rule of 3](https://stackoverflow.com/questions/4172722/what-is-the-rule-of-three). Your assignment operator leaks memory -- in fact, all of that code you have in the assignment operator should have been in the copy constructor. In addition to that, the assignment operator (and copy constructor) should not be checking at all for any mesh conditions -- it should copy whatever is sent to it, good or bad. Otherwise you get fake copies being passed around as legitimate copies, causing some very hard-to-find bugs to occur. – PaulMcKenzie Jul 03 '20 at 07:49
  • So I just wrote this: ``` /* copy constructor */ Mesh2D (const Mesh2D& rhs) { rows = rhs.rows; columns = rhs.columns; mesh = new double[rows*columns]; memcpy(mesh,rhs.mesh,rows*columns*sizeof(double)); std::cout << "Copied!" << std::endl; } ``` Looks right? – Angelos Jul 03 '20 at 07:59
  • You need to add that to your original post, and not in the comment. – PaulMcKenzie Jul 03 '20 at 08:00
  • @PaulMcKenzie Right, I did that. – Angelos Jul 03 '20 at 08:06

2 Answers2

2
  1. inline operator=(const Mesh2D& rhs) is a syntax error, it misses the return type. Also Mesh2D& operator=(const Mesh2D& rhs) is the copy assignment operator, not the copy constructor, that would be Mesh2D(const Mesh2D& rhs).
  2. You can use memcpy, but the last argument should be the correct size rows*colums*sizeof(double). sizeof(rhs) is the size of the class object, which is unrelated to the array size. Since you are inside the copy assignment operator and not inside the copy constructor, the memory for mesh is already allocated, so you should remove mesh = new double [rows*columns]; from the copy assignment operator. You also have to do a return *this; there to match the return type.
  3. Because then you need the const version of the functor: double operator()(size_t i, size_t j) const
  4. read about rule of 3 or rule of 5, you should implement the copy constructor if you have a constructor and destructor.
  5. from @PaulMcKenzie : Use std::vector to not have to do 1, 2, or 4.

Here is a full example: https://ideone.com/seOSwN

mch
  • 9,424
  • 2
  • 28
  • 42
  • 2
    You missed 5). Use `std::vector` to not have to do 1, 2, or 4. – PaulMcKenzie Jul 03 '20 at 07:35
  • @mch But ```inline operator=(const Mesh2D& rhs)``` is compiling fine – Angelos Jul 03 '20 at 07:37
  • @PaulMcKenzie I added that. I thought about that it could be an exercise with the next task to use an `std::vector` to find out how easy it could be. – mch Jul 03 '20 at 07:41
  • @FragiadoulakisAggelos that's because you forgot the `return` statement at the end of the assignment operator. – mch Jul 03 '20 at 07:43
  • @mch You can add to the list the needless check inside the assignment operator for a "bad mesh". The assignment operator has one goal and one goal only -- delete the existing memory and make a copy. If the `Mesh2D` is bad, tough luck, a copy is going to be made, regardless. – PaulMcKenzie Jul 03 '20 at 07:54
2

I will try and add the missing pieces to your code:

  1. The copy constructor

  2. A working assignment operator.

After the code below, I will comment:

#include <iostream>
#include <cstring>
#include <algorithm>

class Mesh2D
{
    private:
    size_t  rows;
    size_t  columns;
    double* mesh;

public:

    Mesh2D(size_t Nx, size_t Ny) : rows(Nx), columns(Ny), mesh(new double[Nx*Ny]{})
    {
        std::cout << "Mesh created." << std::endl;
    }

    Mesh2D(const Mesh2D& rhs) : rows(rhs.rows), columns(rhs.columns), 
                                mesh(new double[rhs.rows * rhs.columns])
    {
       memcpy(mesh, rhs.mesh, (rhs.rows * rhs.columns) * sizeof(double));

       // or better yet
       // std::copy(rhs.mesh, rhs.mesh + rhs.rows * rhs.columns, mesh);
    }
    
    Mesh2D& operator=(const Mesh2D& rhs)
    {
       if ( &rhs != this )
       {
          Mesh2D temp(rhs);
          std::swap(temp.rows, rows);
          std::swap(temp.columns, columns);
          std::swap(temp.mesh, mesh);
      }
      return *this;
    }
       
    ~Mesh2D()
    {
        delete[] mesh;
        std::cout << "Mesh deleted." << std::endl;
    }


    double getrows() const {return rows;}
    double getcolumns() const {return columns;}

    double& operator()(size_t i, size_t j)
    {
        if (i > rows || j > columns){
            throw std::out_of_range("Index exceeds array bounds.");
        }
        return mesh[j + i*columns]; // row major access
    }

    double& operator()(size_t i, size_t j) const
    {  
        if (i > rows || j > columns){
            throw std::out_of_range("Index exceeds array bounds.");
        }
        return mesh[j + i*columns]; // row major access
    }
};

First, note the usage of the member-initialization list to initialize the members of the Mesh2D class. This is a good-habit to do things this way, instead of assigning inside the body of the constructor.


Second, note the assignment operator. It uses the copy / swap idiom to safely and cleanly deallocate the old memory and make a copy.


Third, the operator() should have a const overload, otherwise you could never use your class in something like this:

void foo(const Mesh2D& m)
{
   std::cout << m(0,0);
}

The reason being that m is a const reference, thus you can only call const functions on it. Thus, the operator() needs a const version, along with the non-const version.

Also, the reason why the non-const version of operator() needs to also exist is that you may want to do this:

void foo2(Mesh2D& m)
{
   m(0,0) = 23;
}

If m had only a const version of operator(), the above operation could not be done due to operator() changing the internal state of the object.


Another issue is that you are using memcpy incorrectly. The memcpy works on the number of bytes, not the number of items in an array. Thus you need to multiply by sizeof(double), since that's the byte count that you are copying. A more type-safe solution would be to use std::copy, which automatically uses the correct copying function using the number of items.


Last, the test for a bad "Mesh2D" has been removed from the assignment operator. The reason is that the assignment operator (and copy constructor) job is one thing and one thing only -- make copies.

When you start introducing business logic into the copy-assignment functions, you are risking making copies that are not copies, but instead pseudo-copies of the object being passed-in.

This leads to some of the nastiest and hardest-to-find bugs in C++, and that is when your copy-assignment functions want to play games and not really make copies. You know when you're doing something wrong if your copy constructor or assignment operator is making too many decisions and executing logic based on the passed-in value.


For completion, here is the same version of the class, but using std::vector<double>. Note how much smaller the code is, due to not needing a user-defined assignment operator, copy constructor, or destructor:

#include <iostream>
#include <vector>

class Mesh2D
{
    private:
    size_t  rows;
    size_t  columns;
    std::vector<double> mesh;

public:

    Mesh2D(size_t Nx, size_t Ny) : rows(Nx), columns(Ny), mesh(Nx*Ny)
    {
        std::cout << "Mesh created." << std::endl;
    }

    double getrows() const {return rows;}
    double getcolumns() const {return columns;}

    double& operator()(size_t i, size_t j)
    {
        if (i > rows || j > columns){
            throw std::out_of_range("Index exceeds array bounds.");
        }
        return mesh[j + i*columns]; 
    }

    const double& operator()(size_t i, size_t j) const
    {  
        if (i > rows || j > columns)    {
            throw std::out_of_range("Index exceeds array bounds.");
        }
        return mesh[j + i*columns]; 
    }
};
PaulMcKenzie
  • 34,698
  • 4
  • 24
  • 45
  • Why does the following test program crash? ``` Mesh2D mesh{3,3}; Mesh2D newmesh; newmesh = mesh; PrintMesh2D(newmesh);``` Note that, I am not using the copy assignment operator. If I use it, I am getting garbage values at the console output. Sorry for writing code again in comment. Dont know really how to post this question so that you can see it. – Angelos Jul 03 '20 at 08:40
  • What crash? That program doesn't even compile, due to you trying to create a `Mesh2D` with no arguments. [Here is a full example](http://coliru.stacked-crooked.com/a/19923a98d00666c2) – PaulMcKenzie Jul 03 '20 at 08:43
  • I forgot to mention that I wrote a constructor Mesh2D() : rows(0), columns(0), mesh(nullptr) – Angelos Jul 03 '20 at 09:12
  • [Still no issue](http://coliru.stacked-crooked.com/a/66741f18b11d9f20) – PaulMcKenzie Jul 03 '20 at 09:32
  • I copied and pasted the code directly above and getting garbage values as output. And I don't understand why the program crashes if I comment out the copy assignment operator. Also, why do I need two ()overloads? – Angelos Jul 03 '20 at 09:49
  • You need two overloads because you can't call `operator()` on a const object without the const overload. In addition, you can't assign with `operator()` if the object **is** const. So you need both of them. If you want a real-world example, look at [vector::at()](https://en.cppreference.com/w/cpp/container/vector/at), which is no different. As to removing the copy-assignment operator, the `Mesh2D` object now becomes vulnerable to any copying that may be done when it is removed. That's the whole point of the rule of 3. – PaulMcKenzie Jul 03 '20 at 14:49
  • As to your issue with the garbage values, there is another issue that was created (see the edit). The usage of `memcpy` is not correct. The `memcpy` function works on byte sizes, not the number of elements in the array. But all of these edits shows you how much work it is to do all of these things by hand, rather than simply using `std::vector`. – PaulMcKenzie Jul 03 '20 at 15:10
  • Thank you very much for your time and advice. – Angelos Jul 03 '20 at 17:26