C++ Classes

This exercise introduces you to the creation and use of C++ classes. You need to have done the Introduction to C++ exercise before attempting this.

Creating a Class

  1. Create a file named rect.hpp, containing the following:

    #pragma once
    
    class Rectangle
    {
    };

    Don’t forget the semi-colon at the end!

    Then finish off this class definition so that it is able to represent rectangles. You should assume that a rectangle is represented by the x & y coordinates of its upper-left corner, its width and its height. Create private double fields for each of these.

    Your class should also include

    • A constructor that will create a Rectangle object, given values for x, y, width and height

    • A constructor that will create a Rectangle object, given values for width and height only (x & y defaulting to 0)

    • Getter methods for each of the fields

    • A method called perimeter the returns the perimeter of the rectangle

    • A method called area that returns the area of the rectangle

    Include only prototypes for the methods, not full implementations. Don’t forget to use const where necessary.

    Tip

    Lecture 3 worked through the implementation of a Circle class. This provides a good guide for what you need to do here!

  2. Now create a file named rect.cpp, containing implementations of all the methods defined in Rectangle. Use initializer lists when writing the constructors, and remember that one of them can delegate to the other.

    Check that the class compiles before proceeding further, using

    g++ -c rect.cpp

    If all is well, this should create a file of object code named rect.o. You’ll need this file for the next part of the exercise.

Using The Class

  1. Now create a file named testrect.cpp, containing a small program that tests the Rectangle class. Your program should create two Rectangle objects with different positions and dimensions. It should then print out the perimeter and area of each rectangle.

  2. Compile your program with

    g++ testrect.cpp rect.o -o testrect

    Then enter ./testrect to run it.

Managing Builds With CMake

Since CMake is used to manage the build process for Qt 6 applications, it is worth getting some practice with using it now.

  1. Remove the executable file testrect and object code file rect.o from the directory, then create a new file named CMakeLists.txt in this directory. Give this file the following contents:

    cmake_minimum_required(VERSION 3.16)
    project(rectangle VERSION 1.0 LANGUAGES CXX)
    
    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
    set(CMAKE_CXX_EXTENSIONS OFF)
    
    add_executable(testrect testrect.cpp rect.cpp)

    Line 1 ensures that versions of CMake older than 3.16 cannot be used with this build file. Specifying a minimum version is important if you are using features of CMake that are available only from that version onwards.

    Line 2 specifies the project name and version number, and declares that the project uses C++ (represented here as CXX) rather than C.

    Lines 4–6 together specify that the compiler must, at minimum, offer strict support for the C++11 standard. If you are intending to use features introduced in C++17, for example, then you’ll need to change the ‘11’ to ‘17’.

    Line 8 declares that executable testrect will be built from source files testrect.cpp and rect.cpp. Note that we’ve not included the dependency on rect.hpp here; CMake is smart enough to figure out this dependency for itself.1

  2. Create a build subdirectory and run CMake in it to create the files needed to manage the build. This can be accomplished using the following chained sequence of commands:2

    mkdir build && cd build && cmake ..

    Don’t forget the .. on the end of this!

  3. You should now be in the build subdirectory. Enter ls to list its contents. On Linux or macOS systems, you should see that CMake has created a makefile that can be used to build the application.

    The cross-platform command to execute the build is

    cmake --build .

    However, on systems where a makefile has been generated, you can just enter make instead. Try this now.

  4. Try making a small change to either testrect.cpp or rect.cpp. Save the file, then rebuild. You should see from the displayed output that only the changed file is recompiled to object code.

    However, if you make a change to rect.hpp, both .cpp files will be recompiled, because both of them include (and therefore depend on) rect.hpp.

Note

The nice thing about the approach outlined above is that all of the build artifacts are in the build subdirectory, separate from the source code. This makes clean-up easy: all you need to do is remove build!

Improving The Class

  1. Modify the object construction code so that the values supplied for width and height are validated, with any values that are less than or equal to 0.0 being rejected. If you’ve delegated appropriately, you should need to change only one of the two constructors.

    The most appropriate way of doing this is to throw one of the standard exceptions supported by C++, namely invalid_argument. Here’s an example of how to throw an exception:

    if (x > 100) {
      throw std::invalid_argument("x is too large");
    }

    Note that you will need to have #include <stdexcept> in your .cpp file for this to work. Note also that you can omit the std:: prefix if you have

    using namespace std;

    in your .cpp file.

  2. Modify your Rectangle class so that everything except the constructor that does the validation is inlined into the class definition in rect.hpp. (See Lecture 3 if you’re not sure what inlining is.)

  3. Rebuild testrect and check that its runtime behaviour is unchanged.

  4. Modify the test program so that it attempts to create an invalid rectangle. Rebuild and rerun so that you can see what happens when a C++ exception is thrown.

  5. Add try & catch blocks to the test program, to intercept the exception, display a more helpful message and terminate the program gracefully.

Inheritance & Polymorphism

In this part of the exercise, you will modify the Rectangle class so that it conforms to the inheritance hierarchy presented in Lecture 3:

classDiagram
  Shape <|-- Circle
  Shape <|-- Rectangle
  class Shape {
    xorigin : double
    yorigin : double
    getX() double
    getY() double
  }
  class Circle {
    radius : double
    getRadius() double
  }
  class Rectangle {
    width : double
    height : double
    getWidth() double
    getHeight() double
  }

  1. Download the C++ & Qt lecture material from Minerva. Copy the the files shape.hpp, circle2.hpp and circle2.cpp from the 03/src directory to the directory in which you are currently working.

  2. Modify the Rectangle class so that it inherits from Shape, in the same way that Circle does.

  3. Check that the testrect program still builds and runs as it did before—e.g., by running make in the build subdirectory.

  4. Modify the three classes so that Shape has a pure virtual draw() method (see Lecture 3), which is overridden in Circle and Rectangle. The overridden method in Circle should look something like this:

    void Circle::draw() const
    {
      cout << "Drawing Circle("
           << "x=" << getX()
           << ",y=" << getY()
           << ",r=" << getRadius()
           << ")" << endl;
    }

    The draw() method of Rectangle should behave similarly.

  5. In a file named shapedemo.cpp, write a program that

    • Creates a vector of pointers to Shape
    • Populates that vector with a mixture of Circle and Rectangle objects
    • Loops over the contents of that vector, invoking the draw() method on the referenced objects
  6. Add the following to CMakeLists.txt:

    add_executable(shapedemo shapedemo.cpp circle2.cpp rect.cpp)
  7. In the build directory, rerun CMake, then build and run shapedemo:

    cmake ..
    make shapedemo
    ./shapedemo

    You should see the program demonstrating polymorphic behaviour, whereby the correct version of draw() (for Circle or Rectangle) is always invoked, even though the drawing code is referencing all objects as if they were generic shapes.

Going Further

Tip

This last part of the exercise is optional.

There are some pre-existing methods in this class hierarchy that could be given a similar treatment—i.e., we could implement a pure virtual method in Shape that is overridden in subclasses.

Identify those methods and make the necessary changes to the code. Modify shapedemo.cpp so that these methods are invoked polymorphically.

Footnotes

  1. Although including headers as explicit dependencies isn’t required, it shouldn’t cause problems if you do this, and some IDEs might find helpful for you to do so.↩︎

  2. We could have separated these commands with a semicolon instead, but && makes the execution of the command to the right conditional on the success of the command to the left, which is safer. Of course, you can always just replace this command sequence with three separately-entered commands if you prefer…↩︎