Rule of 3 and destructors

Hello, I'm working on a homework project that involves creating a predator/prey "simulation." It's very basic stuff, but it's meant to test our knowledge of multi-dimensional arrays and working with inheritance. I am limited in what I can use; we cannot use concepts we have not learned, so if I have not used something, then it is because I am not allowed (or haven't learned it).

We need to create a 20x20 board of objects and instantiate them with two types of organisms. The instructions dictate that I must create a parent class Organism and derive the other two classes, Ant and Doodlebug.

I have chosen to approach this by creating a multi-dimensional array of Organisms, and then dynamically replacing each of these objects with Ant and Doodlebug objects. When calling my move() function, I do this...
1
2
3
4
5
6
7
    if (legal) {
        Organism* temp = array[moveChoice];
        array[moveChoice] = this;
        array[currentPosition] = temp;
        temp->setPosition(currentPosition);
        this->setPosition(moveChoice);
    }


*My questions: Do I need the rule of 3 here? Can I have a destructor without worrying about building a copy constructor? And can I delete my array in main without worrying about the rule of 3 at all?*

I have read a lot about the rule of 3 and from what I can tell, I only need to use it if I allocate in the class itself.
Here are the classes I am using, and my main function. The code is not complete, and I am mid-project.

Thank you in advance!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Organism {
public:
    //constructor
    Organism(): hasMoved(false), lifeCycles(0), organismSymbol('-'){}
    //destructor
    ~Organism(){}
    //pure virtual function to give all derived classes the move function
    virtual void move(Organism** array){};
    //pure virtual function to give all derived classes the breed function
    //virtual void breed();
    //setters
    void setHasMoved(bool hasMoved){this->hasMoved = hasMoved;};
    void setlifeCycles(){lifeCycles++;};
    void setOrganismRep(char character){organismSymbol = character;};
    void setPosition(int position){this->position = position;};
    //getters
    bool getHasMoved(){return hasMoved;};
    int getlifeCycles(){return lifeCycles;};
    int getPosition(){return position;};
    char getOrganismRep(){return organismSymbol;};
private:
    bool hasMoved;
    //variable to keep track of number of moves;
    int lifeCycles;
    //what index it's at in the array
    int position;
    //the visual representation of the organism on the board
    char organismSymbol;
};
class Ant: public Organism {
public:
    Ant();
    ~Ant(){};
    void move(Organism** array);
private:

};
class Doodlebug: public Organism{
public:
    Doodlebug();
    ~Doodlebug(){}
    void move(){
        //nothing
    }
private:
};
int main() {
    Organism** organismArray = new Organism*[400];
    for(int i = 5; i < 100 ;i++){
        organismArray[i] = new Ant;
        organismArray[i]->setPosition(i);
    }
    for(int i = 100; i < 400;i++){
        organismArray[i] = new Organism;
        organismArray[i]->setPosition(i);
    }
    //set
    delete[] organismArray;
}
Last edited on
It doesn't look like you need destructors.
What would they do?
There's no dynamic memory to delete (in the individual objects)
or any other resources that need to be cleaned up.
So it looks like you don't need any of the "rule of n" functions.

You say your project is (partly) about "multi-dimensional" arrays.
But it looks like you plan to simulate a 2d array instead of using an actual 2d array.
That's often a good choice, but you should check that it is allowed by your instructor.
Last edited on
Line 48: Why not make this Organism *organismArray[400];?

The other thing that stands out to me is:
1
2
3
4
5
6
7
8
9
10
11
        array[moveChoice] = this;
        this->setPosition(moveChoice);
        array[currentPosition] = temp;
        temp->setPosition(currentPosition);
...

        organismArray[i] = new Ant;
        organismArray[i]->setPosition(i);
...
        organismArray[i] = new Organism;
        organismArray[i]->setPosition(i);

Since placing an Organism in the array requires setting the position, I'd change Organism::setPosition to take the array too:
1
2
3
4
void setPosition(int pos, Organism **array) {
    array[position] = this;
    position = pos;
}



@dutch, Yeah, I am not sure what they would do...my classmates were suggesting that I needed them. But I think I can complete the problem without them.
The homework instructions don't specify multi-dimensional arrays, I was just assuming that was one of the objectives. Can you explain the difference between "simulating" a 2D array and actually using one? Resources I've found online say that having an array of pointers is a multi-dimensional array, and that is what organismArray is.
Also...In a later part of the code I may need to delete an object (Ants can die)...I think I can just use a standard delete keyword for this? Thank you!

@dhayden, I'm not sure I understand your syntax. Is Organism *organismArray[400] a pointer to an array of organisms? As far as I know, this will not work. My array needs to be an array of pointers. If it is not, I cannot fill it with different class objects. If it is just an array of Organisms, I cannot add an Ant, for instance, because it is hard coded to only accept Organisms. However, if it is an array of Organism pointers, then it will accept other types of derived class objects. That's what I was taught in class anyway...so if it's wrong, let me know.

I was conceptualizing setting the position as different from placing the object in the array. I believe I did this so that I could instantiate them separately from placing them in the array...but I think you're right and I should do it all in one function. Thank you!
Last edited on
Well, I'm glad the evil dhayden was finally reported!?!?! :-)
The array he shows is an array of pointers, not a pointer to an array. It's just not dynamically allocated, which doesn't seem to be needed in your case.

As for real 2d array and simulating one, you have a single-dimension in your array. It is an array of 400 pointers. Just because they are pointers doesn't make it multidimensional. In order to access row 7 column 11 (0-based) you would need to access organisms[7 * 20 + 11]. In a normal 2d array you could just say organisms[7][11].

If the size is fixed at 20 by 20 then you could just do:

1
2
3
const int Size = 20; // at top of program so all funcs can see it

Organism* orgs[Size][Size]; // in main 

And it turns out that you should probably have a virtual destructor for Organism in order for it to be able to call the derived classes' destructors (even though they don't have one).

What exactly are the instructions for your assignment?
How are the organisms supposed to act?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <random>
using namespace std;

const int Size = 20;

class Organism {
    int row, col;
public:
    Organism(int r, int c) : row(r), col(c) { }
    virtual ~Organism() = default;
    virtual ostream& print(ostream&) const = 0;
};

ostream& operator<<(ostream& out, const Organism* o) {
    return o->print(out);
}

class Ant : public Organism {
public:
    Ant(int r, int c) : Organism(r, c) { }
    ostream& print(ostream& out) const { return out << 'A'; }
};

class Doodlebug : public Organism {
public:
    Doodlebug(int r, int c) : Organism(r, c) { }
    ostream& print(ostream& out) const { return out << 'D'; }
};

void random_fill(Organism* orgs[][Size]) {
    static default_random_engine rnd{random_device{}()};
    static uniform_int_distribution<int> dist(0, 2);
    for (int r = 0; r < Size; ++r)
        for (int c = 0; c < Size; ++c)
            switch (dist(rnd)) {
            case 0: orgs[r][c] = new Ant(r, c);       break;
            case 1: orgs[r][c] = new Doodlebug(r, c); break;
            case 2: orgs[r][c] = nullptr;             break;
            }
}

void print(Organism* orgs[][Size]) {
    for (int r = 0; r < Size; ++r) {
        for (int c = 0; c < Size; ++c)
            if (orgs[r][c])
                cout << orgs[r][c] << ' ';
            else
                cout << "  ";
        cout << '\n';
    }
}

void delete_orgs(Organism* orgs[][Size]) {
    for (int r = 0; r < Size; ++r)
        for (int c = 0; c < Size; ++c)
            delete orgs[r][c];
}

int main() {
    Organism* orgs[Size][Size] {};  // 2d array of pointers to Organism
    random_fill(orgs);
    print(orgs);
    delete_orgs(orgs);
}

Last edited on
Well, I'm glad the evil dhayden was finally reported!?!?!
DRAT!!! My diabolical plan has been thwarted. Time to retire to my evil lair and devise some new evil C++ help. Bwwaaahhhaaa hhaaaa!!!
:)
@dhayden you got reported? :( I hope I didn't accidentally click that somehow...I actually checked to see if I could "unclick" it and I cannot...so maybe it wasn't me. Oh well, definitely nothing wrong with your answers...Hopefully it doesn't cause you any grief with posting and such.

@dutch, ah ok. When I saw Organism* organismArray[400] I thought it was a pointer to an array. But I see now that is not correct. Yeah, I am not sure why I have dynamically allocated it...I do know the size, and the size is fixed. So I'll probably update that.

I noticed that in your code you're doing something with overloading the << operator. I tried to figure out how to do this for multiple classes, and the answers looked very similar to your code, but I'll be honest...I don't really understand how it works. And frankly, I don't understand why it wouldn't work to just have a cout operator overloaded in both the Organism and Ant class...Shouldn't c++ recognize the types of arguments I'm passing in and run the appropriate function? If I pass in an Ant, why can it not run the ant overloaded cout function?

I'll probably keep with the syntax of a single bracket ([400]) since we've not seen the double bracket style allocation before, and I don't want to get points docked for that. I know in other languages that's used as a way to access multi-dimensional arrays. So array[i][x] would be the value of x at i.

I don't feel comfortable posting the project word for word since other students might be searching for answers, but here is a google doc with the instructions: https://docs.google.com/document/d/12AqeOODT3q_hjQzn9Jcp5OAzRpaJ_F2RggbriJ5lbws/edit?usp=sharing

The organisms are supposed to move according to a turn-based style game, and do various things like breed (create a new organism), eat, or die. So I will have some delete statements, which I think I've handled. For instance, whenever an Ant is "born" I run the below code block.
I includes some other functions for calculating available/nearby spaces.

To be honest, I am not fully sure about how I approached this. I treated the board/grid as a single array, pre-filled with generic Organisms, and then overwritten as necessary. It makes sense to me, but I am sure there is a better way to conceptualize it. I handle out of bounds moves using modulo calculations based on the fixed size of the board.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Ant::breed(Organism**& array, int& AntCount) {
    int currentPosition = this->getPosition();
    int moveChoice = 0;
    vector<int> openMoves;
    getOpenMoves(array, currentPosition, openMoves);
    if(openMoves.size() > 0) {
        moveChoice = openMoves[rand() % openMoves.size()];
        //delete the object that is there
        cout << "*move choice" << array[moveChoice]->getPosition();
        delete array[moveChoice];
        //create an ant
        Ant *ant = new Ant;
        //put it in the new position
        ant->setPosition(moveChoice, array);
        cout << "*AntThatBreeds" << this->getPosition() <<"life: "<<this->getlifeCycles()<<endl;
        cout << "*newAnt" << ant->getPosition() <<"life: "<<ant->getlifeCycles()<< endl;
        AntCount++;
    }
}


Anyway, I already a appreciate all your insight so far, and this is a lengthy response, so no pressure to respond to all my points (or any)! :) Thank you again.
Last edited on
@dhayden,
The "Report" button should be renamed the "Thwart" button.
"From hell's heart, I thwart thee!", stabbing the button.

@memoria, Don't worry about the "report" thing. It's meaningless.

If I pass in an Ant, why can it not run the ant overloaded cout function?

If you really passed an Ant then yes, it would work.
But you aren't passing an Ant.
You are passing an Organism*.
So that's the overload that will be called.
Within that function we call a virtual print function, which dispatches the call to the appropriate derived-class's overload.

I treated the board/grid as a single array, pre-filled with generic Organisms, and then overwritten as necessary

That sounds right to me, as long as you initialize it as per instructions (presumably randomly placing a certain number of ants and doods).

Thanks for the link. I'll read that.
use a 2D array would be a lot easier. You might want to ask the prof or another student if you're allowed to do this.

So far everything you've done looks good. Congratulations.
I don't feel comfortable posting the project word for word since other students might be searching for answers
This isn't related to your question, but I'm curious: what's wrong with other students searching for answers? Feel free to respond in a private message if you like.
Well that was fun. I spent the afternoon coding this up. Here are some comments.

I created a Board class to contain the grid and some methods for using it.

Since most of the Organism methods need access to the board, I stored a reference to the board inside each Organism. That way I don't have to pass it all over the place as a parameter.

Sometimes I needed to know if a grid contained an ant or a doodlebug. I check the character that the organism prints to see which kind it is. This is a bit of a hack.

I created a "findNeighbor() method in the Board class. Given an cell location, it finds a random neighboring cell (up, down, left or right) that is a doodlebug, and ant, or empty, depending on a parameter. I ended up calling this several times.
1
2
3
    // Find a random neighboring (up, down, left or right) cell
    // with the given printChar. Here, printChar 'e' means an empty cell.
    bool findNeighbor(int &r, int &c, char printChar);


I created a "movePos" method in Organism that moves the organism to a new postion. This is similar to your setposition() method, but does a little more:
- set the old position on the board to nullptr
- delete the object in the new position if one is there. This handles the case where a doodlebug eats an ant.
- sets the new grid position to point to the organism.
- sets the organism's row and column members to the new position.

One more one-liner that was handy checks if a number is in bounds. Here it is:
1
2
3
4
5
// see if val is in the interval [low, high)
static inline bool inBounds(int low, int val, int high)
{
    return low <= val && val < high;
}


I made the ant's move logic the default for an Organism. Then my DoodleBug::move() method basically says "if you can eat an ant then do it, otherwise call Organism::move()" If the Ant's move logic was in Ant::move then I wouldn't be able to do that.

To keep things neat and tidy, I created a Board::advance() method that advances the simulation one time interval. This could go into main(), but I think it would clutter it up. Here is my main program:
1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
    srand(time(0));
    Board board(5,100);
    std::string line;
    while (true) {
        board.print(cout);
        cout << "Press Enter to move to the next step, or 'q' to quit: ";
        getline(cin, line);
        if (!cin || line =="q") break;
        board.advance();
    }
}


Note that i got lazy and used srand() and rand(), against all modern advice. I think Jonnin has a nice little toolkit that's just as easy to use which he posted recently.
Last edited on
@dutch & @dhayden -- thank you both for your help, and the encouragement. The instructions don't specify multi-dimensional arrays and I confirmed with a TA that single dimension arrays are fine.

@dutch --that makes sense why it wouldn't work. I'll review your code.

I will probably keep what I have (not use 2D arrays) since it's mostly written, but I will research them more since I like their format.

@dhayden, I only say this because the program TAs and teacher specify that we shouldn't be sharing blocks of code with classmates; so I wouldn't want to be inadvertently involved in a student copying my, or your, answers. The class uses this same project every quarter. I, of course, am asking for help, so I am not opposed to students researching this. That would be hypocritical. But I think if someone found a thread on the exact project, they could read it in good faith and write their own code, or they could copy it. Since I don't really know what type of person would find the thread (maybe one of my current classmates) I think it's better to be discreet. Posting the project, word for word, would make it too easy for someone to find my code. Overly cautious, probably, but saves me the risk of worrying about accidentally "cheating".
//Edit @dhayden...just saw your post saying you coded this! cool...I'm reading it now...
Last edited on
@dhayden...ok...this is mostly how I did things as well.
I didn't implement a board class, although I do think that is the preferred method. I couldn't really conceptualize the idea of moving a reference to the board around (since my array is just my board), so I skipped adding that.

I check the character that the organism prints to see which kind it is.

Yup, I did that too. I even described it as hacky to my fellow classmates :P Other students were dynamically generating bugs at each turn which sounded....like a nightmare.

Your "findNeighbor" function is clever. I ended up doing a couple of separate functions.
isLegalMove = checks if move is within the board
findAnt = creates an array of nearby ants
findOpenSpaces = finds which spaces around the bug that are "open" and don't have any other creature on them.
I haven't fully implemented this, but my logic would be similar for doodlebugs: find an ant, if no ant, check if legal move, move. I was going to put that in my main....but....I might put it under move as you have done.

I used srand and rand for everything. I didn't realize that was taboo!...

Anyway, it gives me confidence that you approached the problem similarly. The output of it is pretty cool--it does give a simple, but good, representation of population growth boom/bust cycles.
Last edited on
One more thing that might need to check: when organisms move, you can't just go through each grid space and move the organism that's there. If you did that, then you'd move some organisms twice. So you need to either go through the organisms (instead of the grid) and move each organism one at a time, or you have to mark each organism after you move it so you don't move it again.
I think if someone found a thread on the exact project [...] they could copy it.

That's a constant problem here. Most of us who provide help try to give code fragments instead of full working solutions because we want people to learn, not just copy & paste. If you're worried about people copying your code, you can always sent it to me in a private message. I suspect dutch would say the same thing.
In my implementation, the board always become stable after a few hundred to a few thousand iterations. Usually the doodlebugs die out but sometimes the ants do. I found another implementation that shows the system going on forever. I'll have to dig deeper to see why this happens.

Maybe it's the way I do the simulation. In each clock cycle, I:
- move all the doodlebugs
- move all the ants.
- For each creature, breed, then die.

I guess the paper you posted implies that it should be:
for each doodlebug:
move, breed, starve
for each ant
move, breed.

Or maybe it should be:
move each doodlebug
breed each doodlebug
starve each doodlebug
move each ant
breed each ant.

Each of these strategies will result in a different simulation.


@dhayden, going on forever? Every time you run it? Does it seed the RNG? (If not it will be the same run every time.)

Anyway, I've been working on it for an hour or so, currently struggling with a b*tch of a bug.
I decided to "simplify" (not sure if it's working out) by not bothering with the class hierarchy.
It's more of an annoyance than a help.
Looks like it's working. The longest run so far was almost 7200 cycles, but the doods always die out.

https://imgur.com/a/B4sEko7

This is how I interpreted the instructions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
antTurns:
    for all ants on the board:
        move
        reproduce

doodTurns:
    for all doods on the board:
        move (possibly eating an ant)
        reproduce
        starve

while neither ants nor doods are all dead:
    antTurns
    doodTurns

In the "move" action, the instructions seem to say to first pick a random direction, then if it's empty to move in that direction, otherwise don't move (even if there are blank squares you could've moved to). That's how I programmed it.
Last edited on
It sounds like dutch and I are getting the same results. I saw one case where all the ants were eaten first, but usually the doodlebugs die.

The instructions say "During one turn, all the doodlebugs should move before the ants do" so Dutch, maybe your loop should start with doodTurns, but the way you've coded it, that only affects the first turn. After that they just ping pong back and forth.

I interpreted the move logic the same way you did, but only for moving, not for eating. So my move for ants is:
1
2
3
pick a random direction
if the cell in that direction is empty then move there
otherwise stay where you are


and for doodlebugs it's:
1
2
3
pick a random neighboring cell that contains an ant
if you found one then move there and eat the ant
else move like an ant does.


So a doodlebug will eat any neighboring ant if one exists. It doesn't choose a random direction first and then eat the ant if one exists in that direction.

I found it odd that ants are either eaten or live forever.

In any case, it's a fun problem to code.
I implemented the move and eating the same way you did (may not move even though there's an empty space; if there's at least one accessible ant, it will be eaten). I made a mistake in my post since I also did the doodTurns before the antTurns, but as you say, it probably doesn't make any real difference.

However, another way to interpret the instructions is like this:

1
2
3
4
5
while at least 1 ant and 1 dood:
  doodMoves
  antMoves
  allReproduce
  doodStarve

Still, it's hard to see that it would make much difference.

BTW, if the imp you saw that ran forever was the heatherhjeong github monstrosity, I doubt it's correct. It's ridiculously boring since it just ends up with the doods running around on a field of ants. Presumably the populations are meant to oscillate.
I have not written a driver program for my code, so I am not sure how many cycles it will run. I think the expected behavior is cyclical and in most cases the bugs do not die off completely.

They have told us the order of operations is...
Move doods
Move Ants
Breed doods
Breed ants
Starve doods

Another thing to consider is that the starve and breed cycles are relative. I realized I was getting visually correct results, but that I was using a mod calculation instead of looking at the total number of simulations minus the lifecycles of a particular bug. All breed and starve cycles are relative to the bug.

I'm going to implement this change today...and I wonder how it will affect my output.
Just a quick bump to say I got a 100 on this assignment :D
Thanks again for everyone's help!
Topic archived. No new replies allowed.