The difficulty of establishing and using polymorphism is on par with other C++ and object-oriented concepts. However, polymorphism is rarely beneficial in small or simple programs, making it challenging to demonstrate its value in an introductory programming text. We can better appreciate its benefits by describing an authentic problem and its solution. To accomplish this, the tiles example excerpts and simplifies a small part of a Mahjong solitaire program.
"Mahjong is a tile-based game that was developed in the 19th century in China and has spread throughout the world." Originally a three- or four-person game, it has become a popular computer solitaire game with numerous variations (see, https://www.free-play-mahjong.com for one example). The following UML class diagram, code fragments, and descriptions are adapted and simplified from a Mahjong solitaire program.
Mahjong consists of 144 tile objects saved in a custom data structure reflecting each tile's position on a three-dimensional game board. The implementation described here instantiates the tiles from ten related classes representing seven different kinds of tiles - one class represents three kinds of tiles, and three classes support inheritance and polymorphism, but the program never instantiates them. However, each tile's position in the data structure and on the game board is independent of their type. Therefore, it's easiest to upcast and represent them collectively in the data structure and program functions as Tile pointers.
In this simplified example, polymorphism makes two essential operations possible. First, the program must draw each tile separately. The Tile draw function draws a blank tile, and the subclass draw functions add tile-specific detail, making their relative execution order significant. Second, to play the game, the player selects the tiles two at a time, and if they "match," the program removes them from the board. What it means to "match" depends on the tile's type and, sometimes, on saved member data. Polymorphism allows the program to test for a match with a single, simple function call. We can solve both problems without polymorphism, but solutions based on branching statements (like switches or if-else ladders) are larger and more cumbersome and complex than polymorphic solutions.
The draw functions use the host operating system's or implementing language's native drawing functions, making the functions non-portable and placing them beyond the scope of an introductory text. The following class outlines include the draw functions to illustrate their syntax and the sequence of their internal operations but otherwise focus on the more easily demonstrated matches functions.
class Tile { public: virtual bool matches(Tile* t) { if (this == t) // (a) return false; if (t == nullptr) // (b) return false; return typeid(*this) == typeid(*t); // (c) } virtual void draw() { // draw a blank tile } };
Tile* t1 = first selected tile; Tile* t2 = second selected tile; if (t1->matches(t2)) { remove(t1); remove(t2); }
t1->matches(t1)
, preventing (c) from returning a false match.nullptr
. The test also prevents a run-time error in (c) if the program inadvertently passes a nullptr
to typeid
.typeid
is a function returning an object describing a data type, including structures and classes. If it returns the same object, the two tiles are instances of the same class. Please note that the function dereferences both pointers before calling typeid
and that ==
forms a Boolean-valued expression, matching the function's return type.class RankTile : public Tile { private: int rank; public: RankTile(int r) : rank(r) {} virtual bool matches(Tile* t) { return Tile::matches(t) && rank == ((RankTile *)t)->rank; } };
Breaking the matches function into sub-expressions makes it easier to understand. For brevity, ellipses replace previously evaluated sub-expressions.
Tile::matches(t)
, calls the Tile class matches function, which returns a Boolean value (true or false).(RankTile *)t
downcasts t from a Tile pointer to a RankTile - the red parentheses form the casting operator. This step is necessary to access the rank member variable.(...)->rank
gets the value saved in tile t's rank member. The black grouping parentheses are necessary to run the casting operation before the higher-precedence arrow selection operator.rank == ...
compares the values saved in this and t's rank members. The sub-expression result is a Boolean value.... && ...
, only evaluates to true when both operands are true. The operation forms a Boolean-valued sub-expression.class CircleTile : public RankTile { public: CircleTile(int rank) : RankTile(rank) {} virtual void draw() { Tile::draw(); // draw "rank" circles on the tile } }; |
class BambooTile : public RankTile { public: BambooTile(int rank) : RankTile(rank) {} virtual void draw() { Tile::draw(); // draw "rank" bamboo sticks on the tile } }; |
class CharacterTile : public Tile { private: char symbol; public: CharacterTile(char c) : symbol(c) {} virtual bool matches(Tile* t) { return Tile::matches(t) && symbol == ((CharacterTile *)t)->symbol; } virtual void draw() { Tile::draw(); // draw "symbol" on the tile } };
Tile::matches(t)
, calls the Tile class matches function, which returns a Boolean value (true or false).(CharacterTile *)t
downcasts t from a Tile pointer to a CharacterTile. This step is necessary to access the symbol member variable.(...)->symbol
gets the character saved in tile t's symbol member. The black grouping parentheses are necessary to cause the casting operation to run before the higher-precedence arrow selection operator.symbol == ...
compares the values saved in this and t's symbol members. The sub-expression produces a Boolean value.... && ...
, only evaluates to true when both operands are true. The operation forms a Boolean-valued sub-expression.class WhiteDragonTile : public Tile { public: virtual void draw() { Tile::draw(); // White dragon decoration on the tile } };
class PictureTile : public Tile { private: string name; Image picture; public: PictureTile(string s) : name(s) { picture = load(name); } virtual void draw() { Tile::draw(); // draw "picture" on the tile } }; |
class SeasonTile : public PictureTile { public: SeasonTile(string s) : PictureTile(s) {} }; |
class FlowerTile : public PictureTile { public: FlowerTile(string s) : PictureTile(s) {} }; |
|
class Bamboo1Tile : public PictureTile { public: Bamboo1Tile() : PictureTile("Sparrow") {} }; |
tiles.cpp includes a main function, making the example compilable and runnable. It demonstrates the C++ syntax for creating various tile object, upcasting them to Tile pointers, and calling the virtual functions.
View | Download |
---|---|
tiles.cpp | tiles.cpp |