views:

314

answers:

4

Hi,

I have created some C++ classes to model a Solitaire game as a learning exercise.

I have classes for the SolitaireGame, a CardStack (one of the 10 piles of cards on the board ) and a Card. My current model states that the SolitaireGame owns a vector of 104 Card objects - which I call the 'shoe'. The SolitaireGame also keeps track of 10 CardStacks which are essentially deque's of addresses of Card objects stored in the shoe. The Deck and Hand inherit from CardStack. I pass the cards from Deck, to Hand to Cascade by means of pointers to original objects stored in the Shoe.

According to a number of answers I received to this question, I should not be passing the Card's around by pointers, but should be using const references. The reason being that objects stored in vectors can have their addresses moved about, so storing their addresses anywhere is a no-no. I recently started looking at boost::sharedptr. What do people think about using shared_ptr to Card here?

Here are simplified versions of the classes:

class SolitaireGame
{
    public: 
    SolitaireGame::SolitaireGame( int numsuits );  

    private:     
        vector<Card> _shoe;
        Deck _deck;
        Hand _hand;
        CardStack _cols[NUM_COLUMNS];
        int _numsuits;
        GameState   gamestate;
 };

class CardStack
{
    public:
        CardStack(){ cout << "CardStack constructor" << endl; }
        CardStack( const CardStack& );
        CardStack( const deque<Card *> &d );
        ~CardStack(){ }

        virtual Card * PullCard( Face f );
        virtual void PushCard( Card * c );

        Card * CardAt( int i ) const;
        Card * Top() const;

        deque<Card *>::iterator Begin() { return _cards.begin(); }
        deque<Card *>::iterator End() { return _cards.end(); }

        int Size() const;
        CardStack& operator=( const CardStack& rhs );

        friend std::ostream& operator<<(std::ostream &os, const CardStack &obj);

private:
        deque<Card *> _cards;

};

A: 

You can't store references in a container, so if you want shared access, you can only use pointers. shared_ptr would seem somewhat meaningless, because you don't want the memory to be managed.

However, if I made a Card class, it would contain just one or two integers, and it would be immutable (unless these are magic cards that can change their suit and value). Therefore I'd only use copies of cards.

When you return, you might prefer a reference (unless you want to store pointers to the returned values, which would end up looking rather bizarre). But personally, again, I'd just return by value.

UncleBens
+2  A: 

Yes if you are taking the address of the elements of your

vector<Card> _shoe;

and placing them into your

deque<Card *> _cards;

There could definitely be a problem, like you describe. Your vector could reallocate, making the address of the Card elements of vector no longer valid.

Passing references (const or otherwise) to the contents of your vector will have the same problems as passing around pointers. In C++ a reference is really a thinly-veiled pointer. The only difference from a pointer being how its used (as an alias) the fact that it can't be "unseated", that is made NULL, and the fact that it's not distinguishable from the aliased type (you can't have a vector of Card references). A reference doesn't have any special reference counting or anything you get with other garbage collected languages. So when your vector reallocates, if anyone holds a reference to any of the cards in the decks, those references will fail just as easily as a pointer would.

Replacing your vector with a vector of boost::shared_ptr's of Cards could resolve your problems. A boost::shared_ptr is reference counted. This means it keeps track of how many referrers exist to the underlying object. And your vector would be a vector of shared_ptrs, not of the object itself. So when the vector get reallocated, you're just adding a new referrer back to the underlying object temporarily during reallocation, and then the vector is replacing the shared_ptr with the shared_ptr living in reallocated space. The underlying object doesn't move.

I would take this a step further and recommend not giving everyone a shared_ptr. Pass around boost::weak_ptr's to the non-owners. A boost::weak_ptr is a weak reference to the underlying data. A weak_ptr gives someone a handle to obtain a shared_ptr when needed. It doesn't participate in the reference count of the underlying data. So you can check, first, if the underlying data was deleted by the owner. Then if it wasn't deleted, obtain a shared_ptr (temporarilly participating in the referrer count) and perform the needed action.

Doug T.
Thanks Doug for the reply. So, if I understood you correctly, you're suggesting that as an extra precaution (even though the shoe is pretty stable) could be to create the shoe as a vector of shared_ptrs to dynamically allocated Cards? And anyone wishing to access these Cards should do so via weak pointers. I like this idea, I think it will be a good learning exercise for me.
BeeBand
+5  A: 

The reason being that objects stored in vectors can have their addresses moved about, so storing their addresses anywhere is a no-no.

Storing (const) references is just as bad as storing pointers for the same reason. If the size of the vector does not change as long as other objects hold pointers to the objects therein, you should be safe.

When programming in C++, you should always decide who “owns” an object, e.g. who is responsible to delete it when it is no longer needed. If there is no natural object owner, you could resort to smart pointers like boost::shared_ptr that use reference counting or garbage collection to manage the object's lifetime.

In your case, it is pretty obvious that the SolitaryGame instance owns all cards. Moreover, the number of cards in the game is fixed. Therefore you can easily pass pointers of your cards to objects that are dependent of the game instance.

Once the game is deleted, all cards will get deleted and remaining pointers will be invalid, but at this time, other objects holding card pointers should get deleted, too.

Ferdinand Beyer
Thanks for the reply Ferdindand. Yes this is how I envisaged it to work, one the game is deleted the cards get deleted but its ok because all the Cascades are deleted too. I figured that my code was reasonably safe as it is since the Cards are not dynamically allocated, and the shoe always contains 104 cards that do not change.
BeeBand
I agree completely with this (+1). The only thing I would add is that I'd pass the cards around as (const) references (yet store them in the CardStack as pointers). My policy is for function arguments and return values to be pointers if and only if NULL is a valid value, otherwise use a reference. This saves the need to check/assert the card is valid before using it, and makes it clear to programmers that NULL is not a valid.
Alex Deem
A: 

I think Ferdinand's answer above is the way to go but I wanted to comment on the use of boost::shared_ptr in this instance.

How heavy are your Card objects? If they're small enough (a few int's say) then it would probably be better just to copy the cards themselves than use boost::shared_ptr since copying boost::shared_ptr's is not cheap (due to thread synchronization on the reference count). This is predicated on your Card objects not needing to have unique identities, e.g. one ten of spades Card object is as good as any other, though.

Brett Hall
Can you elaborate one 'unique identities'? Yes each card is a unique object, although there may be 4 separate 10 of spades Card objects - but none of those 10 of spades objects need to know about the other 3.The Card objects are not heavy - they hold 3 integer values.
BeeBand