Skip to main content
GameDev.net gamedev.net
🔒 Locked

copying a class in c++

Started by algumacoisaqualquer Jan 14, 2007 at 2:19 PM 8 replies 1.6k views
Original Post
algumacoisaqualquer
algumacoisaqualquer
EDIT: I'm starting to think that the problem is actually with the c_player::AddPiece function (the last one in this post). It's a very short function, could someone take a look and tell me if it's unsafe? Ok, basically I want to copy my c_map class. This is done by passing a pointer of a c_map, and all the information is read from that pointer and based on that, the new map stats are set. c_map class has an array of pointer to the c_piece class, and two c_player members (c_player player[0] and player[1]). The class c_player has only one member, and that is an array of c_pieces. Basically, each player has it's pieces and the map (or board) has an array for each square. If there is no piece in that square, the pointer there will point at a special type of piece, the c_piece no_piece, that is a member of map itself. Also, c_piece has only two member, one integer representing the player it belongs to (can be 1, 2 or 0 for the special case no_piece) and one enum representing the piece type (it's not chess but think of it as knight, bishop, queen, etc). The pieces pointer array is declared as c_pieces *pieces[6][6], and points to pieces inside c_player.(std::vector pieces). Well, currently I changes it into boost::array hoping that would solve the problem, but it didn't worked (it is declared now as boost::array,6> pieces;). However, the code I have currently for copying one map into another will corrupt the pieces array for some unknown reason. It basically has two nested loops (i an j, both from 0 to 5), and for each board square, I'll check if it points to no_piece (in this case the map being built will point to it's own no_piece), or if it's a player piece. If it's a player piece, it will make a copy of it, and send it to the corresponding player, and then add that piece just created into the board, at it's place (actually, it's place will point to it). However, after a few loop iterations, the *pieces[6][6] array will get corrupted. The houd 0,0, for instance, that previously pointed into a piece from player 1, of type (any), will now point into a piece that looks like random memory ({alive=true type=-17891602 player=-17891602 }), and all squares will eventually go bad (but they are initially set correctly). Any ideas? Thank you! By the way, here is the copy function:

void c_map::copy(c_map *map)
{
    int total_pieces1 = 0; //total pieces looks dangerous, but should be working fine
    int total_pieces2 = 0;
    for (int i = 0; i < 6; i++)
    {
    	for (int j = 0; j < 6; j++)
    	{
			if(map->pieces[j]->player == 0)
			{
				pieces[j] = &(no_piece);
			}
			if(map->pieces[j]->player == 1)
			{
				piece_type type = map->pieces[j]->type;
				c_piece new_piece(type, 1);
				player[0].AddPiece(new_piece);
				pieces[j] = &player[0].pieces[total_pieces1];//pieces is a array of pointer, we tell pieces[j] to point into the last piece we have created for this player
				total_pieces1++;			
			}
			if(map->pieces[j]->player == 2)
			{
				piece_type type = map->pieces[j]->type;
				c_piece new_piece(type, 2);
				player[1].AddPiece(new_piece);
				pieces[j] = &player[1].pieces[total_pieces2];
				total_pieces2++;			
			}
    	}
    }
    this->player1_turn = map->player1_turn;
    this->how_is_it_going = map->how_is_it_going;
}


and the header:

class c_piece
{
    public:
    bool alive;
    piece_type type;
    int player;
    void Remove();
    c_piece();
    c_piece(piece_type piece, int p_player);
};

class c_player
{
    public:
    std::vector<c_piece> pieces;
    c_player();
    void Reset();
    void AddPiece(c_piece piece);
    void copy(c_player *player);
};

class c_map
{
    c_player player[2];
    game_state how_is_it_going;
    board_state MovementState(int start, int end);
    board_state DoMovement(int start, int end); //DoMovement just moves them
    public:
    board_state MovePieces(int start, int end); //MovePieces checks if movement is legal
    bool IsMovementLegal(int start, int end);
    bool player1_turn;
    c_piece no_piece;
    //c_piece *pieces[6][6];
    boost::array<boost::array<c_piece*,6>,6> pieces;//Changed to boost, but made no difference
    c_map();
    board_state SetMovement(int start, int end);
    game_state GetGameState();
    c_piece GetPiece(int position);
    void Initiate();
    void copy(c_map *map);

};


I think these are working correctly, but if you want to take a look, there are the functions used in the copy function:

//c_piece constructor
c_piece::c_piece(piece_type piece, int p_player)
{
    alive = true;
    type = piece;
    player = p_player;
}

void c_player::AddPiece(c_piece piece)
{
    c_piece new_piece(piece.type, piece.player);
    pieces.push_back(new_piece); //this is an std::vector - he is copying new_piece, not just adding by reference, right?
// I mean, new_piece will get deleted once this function is over, could this be causing the problem?
}


[Edited by - algumacoisaqualquer on January 14, 2007 3:04:45 PM]
Zakwayda
Zakwayda
Based on a quick look at your code, I'm going to guess that the problem is in how you're building the player piece array and storing pointers to the pieces in the map class.

A typical implementation of std::vector may re-allocate its memory on any given call to push_back(); when a re-allocation occurs, all existing pointers to elements of the vector are invalidated. To use your example:
pieces.push_back(piece(...)); // Memory allocatedboard[0][0] = &pieces.back(); // Okpieces.push_back(piece(...)); // Currently allocated memory is usedboard[0][1] = &pieces.back(); // Okpieces.push_back(piece(...)); // vector memory re-allocated: board[0][0] and board[0][1] are now invalid pointersboard[0][2] = &pieces.back(); // This is ok, but may become invalid if the vector continues to grow
There are other operations that will invalidate vector iterators (or pointers to its elements) as well. In short, storing pointers to the contents of a vector is not safe unless it can be guaranteed that no iterator-invalidating operations will be performed (and even then it's probably not the best idea).

I don't know enough about the context to propose a better solution, but generally speaking there are a number of improvements you could make to your code that might make it easier to debug and less prone to error. Perhaps I or someone else will have time to do a quick editing pass on your code and make some suggestions, but meanwhile I would try the following: add the pieces to the player array in one pass, and then assign the pointers in a second pass. This should ensure that the pointers remain valid (although there may be other problems in your code as well that I missed or are not evident from what you posted.)
Bregma
Bregma
Quote:
Original post by algumacoisaqualquer
EDIT: I'm starting to think that the problem is actually with the c_player::AddPiece function (the last one in this post). It's a very short function, could someone take a look and tell me if it's unsafe?


Hmmm, it looks completely safe to me.

I find your c_map::copy member function confusing. Might I recommend you adopt an object-oriented approach to your design to help track down pointer lifetime problems? Any time you use pointers, you should consider writing copy constructors, assignment operators, and destructors. Make each class responsible for copying its own data, don't reply on external manipulation.

So, provide a copy constructor and assignment operator for your c_piece and c_player classes (and use the idiomatic copy constructor for your c_map). Provide proper destructors, and make sure you understand when the destructor is invoked and when your pointers are initialized, modified, or deleted.

--smw
Stephen M. Webb
Professional Free Software Developer
algumacoisaqualquer
algumacoisaqualquer
jyk, thank you so much!! It has been two days that I've been trying to figure out this one... That was exactly what was happening!! My AI is actually working now! (Well, it just gave me an assert error, but that was when it lost the game, not at the first move).

About the pointer problem... now I see that they are quite harder to manage then I tought, but is there any kind of "safe" pointer for this case? Well, as you said, it would require more code to look I guess. Meanwhile, your build player first solution is working fine, thank you!

algumacoisaqualquer
algumacoisaqualquer
Quote:
Original post by Bregma
Quote:
Original post by algumacoisaqualquer
EDIT: I'm starting to think that the problem is actually with the c_player::AddPiece function (the last one in this post). It's a very short function, could someone take a look and tell me if it's unsafe?


Hmmm, it looks completely safe to me.

I find your c_map::copy member function confusing. Might I recommend you adopt an object-oriented approach to your design to help track down pointer lifetime problems? Any time you use pointers, you should consider writing copy constructors, assignment operators, and destructors. Make each class responsible for copying its own data, don't reply on external manipulation.

So, provide a copy constructor and assignment operator for your c_piece and c_player classes (and use the idiomatic copy constructor for your c_map). Provide proper destructors, and make sure you understand when the destructor is invoked and when your pointers are initialized, modified, or deleted.

--smw


Thanks Bregma! But as jyk said, it's not that Addpiece is the problem, but I have pointers to vector elements, and when the vector need to be resized, it will be moved to another place in memory. You probably should have figured this out by now, but I'm just clarifing.

About the constructors - they probably were the best idea, but since I'm not used with them, the first thing I did when thing weren't working was to replace them by vanilla functions in order to check if my implementation of them were wrong.
Zakwayda
Zakwayda
I went ahead and did a quick clean-up pass on your code. It's nothing comprehensive, but I think cleaning things up a bit will help lay the groundwork for further improvements to the overall design and structure of your program (improvements that should make problems like the memory allocation error you encountered less likely to crop up).

The changes I made are commented in the source.

class c_piece{public:    c_piece();    c_piece(piece_type piece, int p_player);    void Remove();    bool       alive;    piece_type type;    int        player;};class c_player{public:    c_player();    void Reset();    //void AddPiece(c_piece piece);    void AddPiece(const c_piece& piece);        // For the most part, the only time you want to pass an argument by pointer is    // when it is not a precondition of the function that the pointer be valid (for    // example, if the argument represents an optional output parameter). That's not the    // case here, so we'll make it a reference (which means it will always be valid under    // normal circumstances), and will also make it constant (which means the object    // cannot be modified within the function). Making the argument a constant reference    // is both safer and more expressive of our intent. (I also made the argument to    // AddPiece() pass-by-reference rather than pass-by-value - this is discussed later.)    //void copy(c_player *player);    void copy(const c_player& player);    std::vector< c_piece > pieces;};class c_map{public:    c_map();    board_state MovePieces(int start, int end);    bool IsMovementLegal(int start, int end);    board_state SetMovement(int start, int end);    game_state GetGameState();    c_piece GetPiece(int position);    void Initiate();    //void copy(c_map *map);    void copy(const c_map& map);    public:    bool    player1_turn;    c_piece no_piece;        // Although the type of array used was not related to your problem, it's certainly a    // good idea to use stuff from Boost where appropriate. Just as a point of interest,    // boost::array is really just a very thin wrapper around a regular C-style array.    // It's main purpose is to provide an interface that a) is similar to std::vector    // (specifically the functions operator[]() and at()), and b) is consistent with other    // standard library containers (begin(), end(), and so on). Regular 'raw' arrays are    // already compatible with most algorithms from the standard library, but with    // boost::array you get greater consistency (and a bit more flexibility, in the sense    // that you can 'typedef' a boost array and then interchange it freely with other    // containers).        //c_piece *pieces[6][6];        enum { BOARD_DIM_X = 6, BOARD_DIM_Y = 6 };    //boost::array<boost::array<c_piece*,6>,6> pieces;    boost::array< boost::array< c_piece*, BOARD_DIM_X >, BOARD_DIM_Y > pieces;    private:    // This is personal preference, but I try to order things in a class header according    // to how 'visible' or 'important to the outside world' they are. A class's public    // interface is obviously of greatest interest to the 'outside world' - in fact, it's    // all that the outside world (aside from friends of the class) knows about the class.        // The same argument applies to protected and private members, although here we're not    // concerned about the outside world, but rather the class itself and any classes that    // inherit from it.        // So I've shuffled things around in your header a bit to reflect this.     board_state MovementState(int start, int end);    board_state DoMovement(int start, int end);    // A standard bit of wisdom: use named constants or enums rather than 'magic numbers'    // where possible. Someone recently put it very well in the For Beginners forum by    // saying, "In general, use a named constant for any value other than 0 or 1". We'll    // go ahead and apply that here (and also earlier with the 'pieces' member variable):        enum { NUM_PLAYERS = 2 };    //c_player player[2];    boost::array< c_player, NUM_PLAYERS > player;    game_state                            how_is_it_going;};// Google 'c++ initializer list' for an explanation of the following syntax:c_piece::c_piece(piece_type piece, int p_player) :    alive(true),    type(piece),    player(p_player){    //alive = true;    //type = piece;    //player = p_player;}// For non-trivial types, you usually want to pass by constant reference rather than by// value, as it's often less costly (since the object doesn't need to be copied).//void c_player::AddPiece(c_piece piece)void c_player::AddPiece(const c_piece& piece){    // push_back() makes a copy of the argument, so there's no need to create a new piece    // here - we can just push back the one passed in:    //c_piece new_piece(piece.type, piece.player);    //pieces.push_back(new_piece);    pieces.push_back(piece);}//void c_map::copy(c_map *map)void c_map::copy(const c_map& map){    // We don't need these, as we'll see in a minute.    //int total_pieces1 = 0;    //int total_pieces2 = 0;        // Personal preference, but I usually use an unsigned type when the intention is that    // values will only ever be non-negative. I think it's clearer.        // Also, it probably doesn't matter in this case whether you use pre- or post-    // increment, but with some types, post-increment may require that a copy of the    // object be made. Therefore it's good to get in the habit of favoring pre-increment    // when the end effect is the same (as is the case here).    //for (int i = 0; i < 6; i++)    for (size_t i = 0; i < BOARD_DIM_X; ++i)    {        //for (int j = 0; j < 6; j++)        for (size_t j = 0; j < BOARD_DIM_Y; ++j)        {            // It doesn't look like the value of 'player' is modified in these blocks of            // code, so these can be if else's (or switch cases):            if(map->pieces[j]->player == 0)            {                // The parentheses aren't necessary here and don't contribute anything in                // terms of clarity, IMO.                //pieces[j] = &(no_piece);                pieces[j] = &no_piece;            }            //if(map->pieces[j]->player == 1)            else if(map->pieces[j]->player == 1)            {                // The next block can be cleaned up in a couple of ways. First of all, we                // can construct a temporary object for the argument to AddPiece(), saving                // a couple of lines. Secondly, the vector function back() obviates the                // need for the total_pieces1 variable.                // Also, 'magic numbers' are showing up again. I had to go look at the                // declaration for the piece class to see what the '1' meant, whereas if                // it were a named constant - say, PLAYER_ONE - it would have been                // immediately clear. So this is a case where even the literal '1' would                // be better off as a named constant :)                                //piece_type type = map->pieces[j]->type;                //c_piece new_piece(type, 1);                //player[0].AddPiece(new_piece);                //pieces[j] = &player[0].pieces[total_pieces1];                //total_pieces1++;                                player[0].AddPiece(c_piece(map->pieces[j]->type, 1));                pieces[j] = &player[0].pieces.back();                                // We've of course already addressed the logic error here concerning                // vector memory re-allocation.            }            //if(map->pieces[j]->player == 2)            else if(map->pieces[j]->player == 2)            {                //piece_type type = map->pieces[j]->type;                //c_piece new_piece(type, 2);                //player[1].AddPiece(new_piece);                //pieces[j] = &player[1].pieces[total_pieces2];                //total_pieces2++;                player[1].AddPiece(c_piece(map->pieces[j]->type, 2));                pieces[j] = &player[1].pieces.back();                      }        }    }        // Use of the 'this' pointer isn't necessary here:    //this->player1_turn = map->player1_turn;    //this->how_is_it_going = map->how_is_it_going;    player1_turn = map->player1_turn;    how_is_it_going = map->how_is_it_going;}// Issues left unaddressed include some aspects of constant correctness, public vs.// private member data, and naming conventions for classes and member variables.
Zahlman
Zahlman
Shouldn't that .copy() member function be an assignment operator and/or copy constructor instead? :s
algumacoisaqualquer
algumacoisaqualquer
Well jyk, thanks for the extra tips! Actually, I had never really understood what the const keyword was doing at the function declaration until now! If you want to take a look, here's how my copy function works right now:
void c_map::copy(const c_map& map){    int piece_index1 = 0;    int piece_index2 = 0;    for (size_t i = 0; i < BOARD_X_SIZE; ++i)    {    	for (size_t j = 0; j < BOARD_Y_SIZE; ++j)    	{			if(map.pieces[j]->player == PLAYER_ONE)			{	//(player[0] is actually player one)				player[0].AddPiece(c_piece(map.pieces[j]->type, PLAYER_ONE));					}			else if(map.pieces[j]->player == PLAYER_TWO)			{	//(player[1] is actually player two)				player[1].AddPiece(c_piece(map.pieces[j]->type, PLAYER_TWO));				}    	}    }		for (size_t i = 0; i < BOARD_X_SIZE; ++i)    {    	for (size_t j = 0; j < BOARD_Y_SIZE; ++j)    	{			pieces[j] = &(no_piece);			if(map.pieces[j]->player == PLAYER_ONE)			{				//piece-index is a hack, but it works, because the pieces will be read				//at the same order that they were placed on player.pieces.				pieces[j] = &player[0].pieces[piece_index1];				piece_index1++;			}			else if(map.pieces[j]->player == PLAYER_TWO)			{				pieces[j] = &player[1].pieces[piece_index2];				piece_index2++;			}    	}    }    player1_turn = map.player1_turn;    how_is_it_going = map.how_is_it_going;}

It is basically the same thing as you passed to me, but I'm doing two separate loops, one for indexing the pieces at player, and another to index them at my pointers array (because of that push_back reallocating memory problem you mentioned earlier). Also, I'm declaring my magic numbers now as "const int PLAYER_ONE = 1;", is there a problem with this? I mean, the only one I see is that they are globals, but since their values won't be changing, this isn't suposed to be an issue, right? Anyway, thanks a lot for the effort you had.

Oh, and also because of the two loops, I had to mantain the piece_index1 variable (it had a different name, but same purpose), as the player[n].pieces.back() function will not work anymore.

Zahlman: You are right, the only reason why the = operator isn't overloaded is because I don't know how overloading works. I mean, I know the concept, but I have never done any serious overloading before... but I'm really considering to do this at this project, see if I learn how to do it.
Zahlman
Zahlman
It's quite easy.

// void c_map::copy(const c_map& map) {c_map& c_map::operator=(const c_map& map) {  // Copying logic goes here.  return *this; // allows for "chaining" the operator, i.e. 'a = b = c',  // and also for things like "while ((foo = get_map())) {", if your object  // is interpretable as a boolean (can be arranged in several ways).}


However, the "copying logic" may be more complex than is needed. In general, (a) a class that requires any of an assignment operator, copy constructor or destructor will need all three; and (b) copy constructors and assignment operators tend to do very similar work.

One common strategy for the assignment operator is to use the "copy and swap idiom". This allows for exception safety (e.g. if making the copy requires some dynamic allocation of memory - say your class holds a dynamically allocated resource; the copy should get a separate resource with the same contents) and automatically protects against self-assignment (bugs that happen when the case 'a = a;' is not taken into consideration). The idea is to implement the assignment operator as follows:

- Create a temporary copy of the RHS (with the copy constructor).
- Swap data members between the LHS (the this-object) and the temporary copy. (Primitive values just be assigned from copy to LHS. For the others, you normally want to use either std::swap() or a .swap() member function of the member.)
- Return self by reference, for chaining.

The idea is that at the end of the function, the copy holds all the resources that used to be held by the this-object, and then it gets destructed, therefore cleaning up exactly the set of things that needs to be. In the case of self-assignment, you swap all the members with a copy of the this-object, leaving the this-object in the same state (since you swapped back in copied data). If an exception gets thrown from the constructor (typically because of a memory allocation problem), then RHS and LHS are both correctly untouched, and as long as the constructor is exception safe, nothing gets leaked by the partially-constructed temporary.

That could look like:

class Holder {  Thing* t;  // Omitted other constructors  // Putting the destructor here requires that the full declaration of Thing  // has already been seen in this translation unit.  ~Holder() { delete t; } // every instance "owns" its Thing.  // To copy-construct, we copy-construct the thing dynamically, and point at  // it. Thus we get a reference to the RHS's Thing, and pass it to the Thing  // copy constructor in a 'new' expression.  Holder(const Holder& h) : t(new Thing(*(h.t))) {}  // You can only safely initialize ONE dynamic allocation in an initializer  // list. If a second or subsequent allocation failed, the ones before it  // would leak. When the exception is thrown, destructors are called for the  // previously-constructed members/bases, BUT your members are pointers in  // this case, so no destruction happens of the previous allocation.  // The assignment operator works like this:  Holder& operator=(const Holder& rhs) {    Holder h(rhs); // copy    std::swap(t, h.t); // swap    return *this; // chain.  }};
algumacoisaqualquer
algumacoisaqualquer
Zahlman, thanks for the code! I didn't implemented it at first, as I was trying to debug my program for quite some time now (this was yet another bug, not the one I mentioned earlier), but now that it is working, I replaced my copy function by the overloaded operator. It's working excatly the same way, there wasn't any problems with the exchanging!

I don't think I'll run into things like a = a, as my map classes only recieve values at initialization (yet another thing I have to fix - moving things back to the contructor class), so this overloading should work fine. However, I did not understand what you were talking above. I mean, what does RHS and LHS means? I'm assuming that they are just the passed objects into a swap function, or maybe to the operator=. I mean, by this method, I would still need to maintain a copy function right? Is this what Holder(const Holder& h) : t(new Thing(*(h.t))) {} does? Hmm.... come to think about it, I think I got the function: we just create a holder that will be deleted soon, but for some time it will contain the passed values, from the passed holder. Then we swap his values with the values of this-> class, so that this-> now holds the values from the passed holder, is that correct?

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.