Lab Exercise 5 - OOP
We will develop a class hierarchy of polygons and add some standard behaviour to the polygons using operator overloading as well as pure virtual functions. We will also exercise functors and lambdas as part of the exercise.
Create a new Visual Studio project named polygon and configure the project as done in previous lab exercises.
Create a new header file that will define the abstract base Polygon class. For illustration purpose, we will also implement the move constructor and assignment operators for polygons.
Note: Polygon storing width and height member variables is not a great choice. We do this purely to illustrate use of protected accessor functions.
#pragma once namespace csc240 { class Polygon { public: virtual double area() const = 0; virtual ~Polygon() {} Polygon( const Polygon& ) = delete; Polygon& operator= ( const Polygon& ) = delete; Polygon( Polygon&& other ) noexcept { std::swap( width, other.width ); std::swap( height, other.height ); } Polygon& operator= ( Polygon&& other ) noexcept { std::swap( width, other.width ); std::swap( height, other.height ); return *this; } protected: Polygon( float width, float height ) : width{ width }, height{ height } {} float getWidth() const { return width; } float getHeight() const { return height; } private: friend std::ostream& operator<< ( std::ostream&, const Polygon& ); friend bool operator== ( const Polygon&, const Polygon& ); float width; float height; }; inline std::ostream& operator<< ( std::ostream& stream, const Polygon& polygon ) { stream << typeid( polygon ).name() << " - width: " << polygon.width << ", height: " << polygon.height; return stream; } inline bool operator== ( const Polygon& lhs, const Polygon& rhs ) { return ( typeid( lhs ) == typeid( rhs ) ) && ( lhs.width == rhs.width ) && ( lhs.height == rhs.height ); } inline bool operator!= ( const Polygon& lhs, const Polygon& rhs ) { return !( lhs == rhs ); } inline bool operator< ( const Polygon& lhs, const Polygon& rhs ) { return lhs.area() < rhs.area(); } }
Create a new header file to define a Rectangle. Rectangle inherits from Polygon and implements the required area function.
#pragma once #include "Polygon.h" namespace csc240 { class Rectangle : public Polygon { public: Rectangle( float width, float height ) : Polygon( width, height ) {} double area() const override final { return getWidth() * getHeight(); } }; }
Create a new header file to define the Square class. Square inherits from Rectangle (a square is a special case of a rectangle with identical dimensions for length and breadth). We will also define a isSquare
function to illustrate polymorphic access in our unit test suite.
Note: Square inheriting from Rectangle is in general a poor idea (even though in geometry a square is a special type of rectangle with identical width and height. In our example it is fairly safe, since we do not allow modification of width
or height
properties. If there were setWidth
and setHeight
functions, square would need to redefine both to also modify the complimentary dimension which makes inheriting from rectangle not worthwhile. This is sometimes referred to as “almost IS-A” relationship.
#pragma once #include "Rectangle.h" namespace csc240 { class Square : public Rectangle { public: explicit Square( float side ) : Rectangle( side, side ) {} static bool isSquare() { return true; } }; }
Create a new header file to define a Triangle. Triangle inherits from Polygon and implements the required area function.
#pragma once #include "Polygon.h" namespace csc240 { class Triangle : public Polygon { public: Triangle( float width, float height ) : Polygon( width, height ) {} double area() const override { return getWidth() * getHeight() / 2.0; } }; }
Create a new header file to define a Pentagon. Pentagon inherits from Triangle even though pentagons are not triangles. This is purely to illustrate use of super-class functions from a child class and is not proper use of OOP. Similar to Square, we will implement a isPentagon
function that we will use to illustrate polymorphism.
#pragma once #include "Triangle.h" namespace csc240 { class Pentagon : public Triangle { public: Pentagon( float side, float apothem ) : Triangle( side, apothem ) {} double area() const override final { return 5 * Triangle::area(); } static bool isPentagon() { return true; } }; }
Unit Tests
Create a new source file to hold the unit test suite for a few of the operators supported by Polygon. We will ensure that comparisons between different types of polygons always fail, while identical ones of the same type should always return true. Similarly we will ensure that stream output differs based on the type of polygon.
#include "catch.hpp" #include "Pentagon.h" #include "Square.h" #include <sstream> namespace csc240 { SCENARIO( "Equality operator for polygon" ) { GIVEN( "Polymorphic instances of polygon" ) { THEN( "Equality operator differentiates between rectangle and square" ) { REQUIRE( Rectangle( 2, 2 ) != Square( 2 ) ); } AND_THEN( "Equality operator differentiates between triangle and pentagon" ) { REQUIRE( Triangle( 2, 2 ) != Pentagon( 2, 2 ) ); } AND_THEN( "Equality operator works on instances of same types" ) { REQUIRE( Rectangle( 2, 2 ) == Rectangle( 2, 2 ) ); REQUIRE( Square( 2 ) == Square( 2 ) ); REQUIRE( Triangle( 2, 2 ) == Triangle( 2, 2 ) ); REQUIRE( Pentagon( 2, 2 ) == Pentagon( 2, 2 ) ); } } } SCENARIO( "Stream output operator for polygon" ) { GIVEN( "Polymorphic instances of polygon" ) { THEN( "Stream output operator produces different output for rectangles and squares" ) { std::stringstream rs; rs << Rectangle{ 2, 2 }; std:std::stringstream ss; ss << Square{ 2 }; REQUIRE( rs.str() != ss.str() ); } AND_THEN( "Stream output operator produces different output for triangles and pentagons" ) { std::stringstream ts; ts << Triangle{ 2, 3 }; std::stringstream ps; ps << Pentagon{ 2, 3 }; REQUIRE( ts.str() != ps.str() ); } AND_THEN( "Stream output operator produces identical output for identical rectangles" ) { std::stringstream rs1; rs1 << Rectangle{ 2, 2 }; std::stringstream rs2; rs2 << Rectangle{ 2, 2 }; REQUIRE( rs1.str() == rs2.str() ); } } } }
Create a new source file to hold the test suite for casting of polymorphic types. Note the two types of tests we do using dynamic_cast
.
#include "catch.hpp" #include "Pentagon.h" #include "Square.h" namespace csc240 { SCENARIO( "static_cast of polymorphic instances" ) { GIVEN( "Polymorphic instance of polygon" ) { std::unique_ptr<Polygon> p = std::make_unique<Pentagon>( 2, 2 ); THEN( "static_cast allows access of concrete instance functions" ) { REQUIRE( static_cast<Pentagon*>( p.get() )->isPentagon() ); } } } SCENARIO( "dynamic_cast of polymorphic instances" ) { GIVEN( "Polymorphic instances of polygon" ) { std::unique_ptr<Polygon> p = std::make_unique<Square>( 2 ); THEN( "dynamic_cast allows access to concrete instance functions" ) { REQUIRE( dynamic_cast<Square*>( p.get() )->isSquare() ); } THEN( "dynamic_cast returns null for invalid type" ) { REQUIRE_FALSE( dynamic_cast<Pentagon*>( p.get() ) ); } AND_THEN( "dynamic_cast throws exception for invalid reference type" ) { REQUIRE_THROWS( dynamic_cast<Pentagon&>( *p.get() ) ); } } } }
Create a new source file for test suite related to computing area for different types of polygons. We will once again ensure that polymorphic computation of area is deterministic.
#include "catch.hpp" #include "Square.h" #include "Pentagon.h" #include <memory> #include <iostream> #include <cmath> namespace csc240 { SCENARIO( "Compute area of a rectangle" ) { GIVEN( "Rectangles of different dimensions" ) { THEN( "Computed area is correct" ) { REQUIRE( 4 == Rectangle( 2, 2 ).area() ); REQUIRE( 24 == Rectangle( 8, 3 ).area() ); REQUIRE( 42.75 == Rectangle( 4.5, 9.5 ).area() ); } } } SCENARIO( "Compute area of a square" ) { GIVEN( "Squares of different dimensions" ) { THEN( "Computed area is correct" ) { REQUIRE( std::pow( 2, 2 ) == Square( 2 ).area() ); REQUIRE( std::pow( 4, 2 ) == Square( 4 ).area() ); REQUIRE( std::pow( 9, 2 ) == Square( 9 ).area() ); } } } SCENARIO( "Compute area of a triangle" ) { GIVEN( "Triangles of different dimensions" ) { THEN( "Computed area is correct" ) { REQUIRE( 30 == Triangle( 15, 4 ).area() ); REQUIRE( 27 == Triangle( 6, 9 ).area() ); REQUIRE( 20 == Triangle( 5, 8 ).area() ); REQUIRE( 18 == Triangle( 3, 12 ).area() ); } } } SCENARIO( "Compute area of a pentagon" ) { GIVEN( "Pentagons of different dimensions" ) { THEN( "Computed area is correct" ) { REQUIRE( 150 == Pentagon( 15, 4 ).area() ); REQUIRE( 135 == Pentagon( 6, 9 ).area() ); REQUIRE( 100 == Pentagon( 5, 8 ).area() ); REQUIRE( 90 == Pentagon( 3, 12 ).area() ); REQUIRE( 37.5 == Pentagon( 5, 3 ).area() ); } } } SCENARIO( "Compute area of polygon" ) { GIVEN( "Polymorphic polygon instances" ) { THEN( "Computed area is correct" ) { std::unique_ptr<Polygon> p = std::make_unique<Rectangle>( 2, 2 ); REQUIRE( 4 == p->area() ); p = std::make_unique<Rectangle>( 8, 3 ); REQUIRE( 24 == p->area() ); p = std::make_unique<Rectangle>( 4.5, 9.5 ); REQUIRE( 42.75 == p->area() ); p = std::make_unique<Triangle>( 15, 4 ); REQUIRE( 30 == p->area() ); p = std::make_unique<Triangle>( 6, 9 ); REQUIRE( 27 == p->area() ); p = std::make_unique<Triangle>( 5, 8 ); REQUIRE( 20 == p->area() ); p = std::make_unique<Triangle>( 3, 12 ); REQUIRE( 18 == p->area() ); p = std::make_unique<Square>( 3 ); REQUIRE( std::pow( 3, 2 ) == p->area() ); p = std::make_unique<Square>( 16 ); REQUIRE( std::pow( 16, 2 ) == p->area() ); p = std::make_unique<Pentagon>( 3, 2 ); REQUIRE( 15 == p->area() ); } } } SCENARIO( "Polygons with same dimensions have different area" ) { GIVEN( "Rectangles and triangles with similar dimensions" ) { THEN( "Computed area differs" ) { REQUIRE( Rectangle( 2, 2 ).area() != Triangle( 2, 2 ).area() ); REQUIRE( Rectangle( 8, 3 ).area() != Triangle( 8, 3 ).area() ); REQUIRE( Rectangle( 4.5, 9.5 ).area() != Triangle( 4.5, 9.5 ).area() ); REQUIRE( Rectangle( 15, 4 ).area() != Triangle( 15, 4 ).area() ); REQUIRE( Pentagon( 15, 4 ).area() != Triangle( 15, 4 ).area() ); } } } }
Create a new source file to test sorting of Polygon instances stored in a container using functor or lambda. Note that in this example we have used to trailing return type function declaration in our functor.
#include "catch.hpp" #include "Pentagon.h" #include "Square.h" #include <vector> namespace csc240 { using Pointer = std::unique_ptr<Polygon>; using Polygons = std::vector<Pointer>; struct Less { auto operator() ( const Pointer& lhs, const Pointer& rhs ) const -> bool { return *lhs < *rhs; } }; void check( const Polygons& polygons ) { REQUIRE( polygons[0]->area() == 15 ); REQUIRE( polygons[1]->area() == 16 ); REQUIRE( polygons[2]->area() == 20 ); REQUIRE( polygons[3]->area() == 27 ); } Polygons create() { Polygons polygons; polygons.emplace_back( std::make_unique<Rectangle>( 2, 10 ) ); polygons.emplace_back( std::make_unique<Square>( 4 ) ); polygons.emplace_back( std::make_unique<Triangle>( 6, 9 ) ); polygons.emplace_back( std::make_unique<Pentagon>( 3, 2 ) ); return polygons; } SCENARIO( "Sort polygons using functor" ) { GIVEN( "A container of polygon instances" ) { Polygons polygons = create(); THEN( "Container can be sorted using functor" ) { std::sort( polygons.begin(), polygons.end(), Less{} ); check( polygons ); } } } SCENARIO( "Sort polygons using lambda" ) { GIVEN( "A container of polygon instances" ) { Polygons polygons = create(); THEN( "Container can be sorted using lambda" ) { std::sort( polygons.begin(), polygons.end(), [&] ( const Pointer& lhs, const Pointer& rhs ) { return lhs->area() < rhs->area(); } ); check( polygons ); } } } }
#define CATCH_CONFIG_MAIN#include "catch.hpp"
Add additional tests for the Polygon operators to make the suite complete.