C++ Circle Code Reorganisation

Simple Reorganisation of Circle Calculator

Algorithmically similar programs, with the same functionality, can be organised differently. Some of the various organisational possibilities are demonstrated here, culminating with the most common practical organisation: multi-file projects. We have also included an option us­ing simple encapsulation.

PREREQUISITES — You should already…
  • have non-trivial C programming experience.
  • be familiar with command line basics in a POSIX shell, Command Prompt, or PowerShell.

Introduction

A C++ compiler reads one file, once, from top to bottom, left to right. Within this frame­work, it does not care how you organise your code. Here we show how the same, simple, program can be re­ar­ran­ged, and still be the same program, i.e., perform exactly the same tasks. We also include a simple ob­ject-oriented organisation, which may be ignored in the short-term, depending on your cur­rent ex­pe­rience.

Simplest Version

In this version, all the code for the execution of the program tasks is in the main() function. It per­forms all I/O and calculations directly.

simpcirc.cppSimplest Circle Example
/*!@file  simpcirc.cpp
*  @brief [c++11] Simplest Circle Calculator
*/
#include <iostream>

#define PI 3.14159265359  // C-style symbolic constant (macro).

int main () {
   using std::cout;  using std::cin;  using std::endl;

   double radius;
   cout << "Radius?: ";
   if (!(cin >> radius)) {
      std::cerr << "Invalid input for radius. Terminating.\n";
      return EXIT_FAILURE;
      }

   double circum = 2.0 * PI * radius;
   double area = PI * radius * radius;

   cout << "Radius : " << radius << endl;
   cout << "Circum.: " << circum << endl;
   cout << "Area   : " << area << endl;

   return EXIT_SUCCESS;
   }

For such a simple program, that might be all you would ever need. It is the principle that counts, however, and the benefits of reorganisation simply increase with program complexity.

Using Functions

Whenever we foresee or observe the some code used multiple times, we should place it in func­tions. The same program above is presented again below, but it now uses functions. By design, we wanted to read the program from the top, which means we wanted to define the helper func­tions lower down in the file. For that to work, we still had to declare the functions before main().

funccirc.cppCircle with Functions Example
/*!@file  funccirc.cpp
*  @brief [c++11] Circle Calculator Using Functions
*/
#include <iostream>

extern double circcircum (double);      // param names optional in function
extern double circarea (double radius); // decl's, but aids readability.

constexpr double PI = 3.14159265359;    // C++11-style symbolic constant.

int main () {
   using std::cout;  using std::cin;  using std::endl;
   
   double radius;
   cout << "Radius?: ";
   if (!(cin >> radius)) {
      std::cerr << "Invalid input for radius. Terminating.\n";
      return EXIT_FAILURE;
      }

   double circum = circcircum(radius);
   double area = circarea(radius);

   cout << "Radius : " << radius << endl;
   cout << "Circum.: " << circum << endl;
   cout << "Area   : " << area << endl;

   return EXIT_SUCCESS;
   }

double circcircum (double radius) {
   double result;                       // not really necessary for
   result = 2.0 * PI * radius;          // such a simple function, but
   return result;                       // is more representative.
   }

double circarea (double radius) {
   return PI * radius * radius;         // simpler than above version.
   }

The circcircum() function is more canonical (typical), than the circarea() function. For func­tions this simple, the latter is generally better, but the first represents a more common pattern for more complex functions.

Separate Files

C++ supports separate compilation, which really means that it is not very sophisticated, and nev­er ev­er has any concept of a “program” — it only compiles source files to object files, one at a time, with no memory of the previous files it compiled. The linker makes executables from the object files.

The file containing main() still needs to know about the functions to be called. But instead of keep­ing the declarations in this file, we moved it to our own include file (circfuncs.hpp). The pre­pro­ces­sor will then replace the #include… line with the contents of the file.

main.cppCircle with Functions Main File
/*!@file  main.cpp
*  @brief [c++11] Circle Calculator Using Functions
*/
#include <iostream>
#include "circfuncs.hpp"

int main () {
   using std::cout;  using std::cin;  using std::endl;
   
   double radius;
   cout << "Radius?: ";
   if (!(cin >> radius)) {
      std::cerr << "Invalid input for radius. Terminating.\n";
      return EXIT_FAILURE;
      }

   double circum = circcircum(radius);
   double area = circarea(radius);

   cout << "Radius : " << radius << endl;
   cout << "Circum.: " << circum << endl;
   cout << "Area   : " << area << endl;

   return EXIT_SUCCESS;
   }

The circfuncs.cpp file contains the definitions of the circle calculator functions we may want to re­use in programs. Although not syntactically necessary, we always include the head­er file de­clar­ing the functions in the file defining the functions, so the compiler can check that they match.

circfuncs.cppCircle with Functions Implementation
/*!@file  circfuncs.cpp
*  @brief [c++11] Circle Calculator Function Definitions
*/
#include "circfuncs.hpp" // good practice & for `PI` in this case.

double circcircum (double radius) {
   double result;
   result = 2.0 * PI * radius;
   return result;
   }

double circarea (double radius) {
   return PI * radius * radius;
   }

IMPORTANTPaired Specification & Implementation Files

We cannot stress enough the importance of including xxx.hpp into its cor­re­spon­d­ing xxx.cpp file. It is not always syntactically necessary, but this is the only way we can ensure that dec­la­ra­tions and definitions remain in sync. If there is an unintentional mismatch that is not de­tec­ted, you will have a very-difficult-to-find bug.

circfuncs.cppCircle with Functions Specification
/*!@file  circfuncs.hpp
*  @brief [c++11] Declarations of Circle Calculator Functions
*/
#if !defined _CIRCFUNCS_HPP_
    #define  _CIRCFUNCS_HPP_
constexpr double PI = 3.14159265359;
extern double circcircum (double radius);
extern double circarea (double radius);
#endif // _CIRCFUNCS_HPP_

The header file contains an inclusion guard to prevent multiple inclusions in the same com­pi­la­tion unit. The macro name (in this case _CIRCFUNCS_HPP_) is derived from the file name, to help ensure uniqueness. It is a well-es­tab­lish­ed convention, and an easy pattern to implement, even if you might not yet understand the full rationale.

Compiling Multi-File Programs

You can simply add all the source files in your program to the end of your normal command line. This disadvantages compile times, especially when there are many source files. For something this sim­ple, it could look like this:

$> BUILD CIRCLE (1)
g++ -Wall -Werror -std=c++11 -o circle main.cpp circfuncs.cpp

You could also compile each source file (.cpp) separately, but then you must instruct the driver to leave the objects files, and not delete them after the linker is done:

$> BUILD CIRCLE (2)
g++ -Wall -Werror -std=c++11 -c main.cpp
g++ -Wall -Werror -std=c++11 -c circfuncs.cpp
g++ -o circle main.o circfuncs.o

The first two commands create main.o and circfuncs.o respectively, because of the -c switch (means “compile only”, i.e., don't call the linker). The last command only calls the linker with ob­ject files as arguments — this effective means “linking”, since the driver (g++), will only call the link­er, when given only object files as arguments.

IMPORTANTDo Not Compile Header Files

You never mention header files on the compilation command line. They are not source files in the normal sense, but rather components to be included in other .cpp files.

Encapsulation of Circle

Once some C++ object-oriented features are discovered, further organisation becomes available. In particular, encapsulation, using a class, is now possible. It is generally quite common to re­strict a .cpp file to one class (sometimes called the implementation), with its cor­re­spon­ding head­er file (sometimes called the specification), just like the ordinary multi-file organisation above.

main.cppCircle Encapsulation Main
/*!@file  main.cpp
*  @brief [c++11] Circle Client Code
*/
#include <iostream>
#include <iomanip>
#include <new>
#include "Circle.hpp"

int main ()
   try{
      Circle C1{1.2};        // define and init  `Circle` object: `C1`.
      Circle C2;             // define and default init `C2` (`…C2{};`).
      Circle C3{C1};         // define and init with `C1`'s value.
      Circle* P{&C1};        // define and init a pointer to `Circle`.

      C2 = C1;               // like `struct`s, copy-assignment works.
      C2.set_radius(2.1);    // change copied radius.
      P->set_radius(3.4);    // change `C1`'s radius via pointer `P`.

      using std::cout;  using std::setw;
      cout << std::fixed << std::setprecision(8);
      int w = (int)log10(C2.area()) + cout.precision() + 2; 
      cout << "C2.radius() = " << setw(w) << C2.radius() << '\n';
      cout << "C2.circum() = " << setw(w) << C2.circum() << '\n';
      cout << "C2.area()   = " << setw(w) << C2.area()   << '\n';

      cout << "C2 = " << C2 << std::endl;

      C2.set_radius(-12.34); // force exception for testing

      return EXIT_SUCCESS;
      }
   catch (std::exception& ex) {
      std::cerr << "Exception: " << ex.what() << std::endl;
      return EXIT_FAILURE;
      }

The main() function above simply exercises some features of the encapsulated Circle class. Al­though not shown, it should be clear that we can have arrays of Circle ob­jects; pass Circle objects as pa­ra­me­ters; and return Circle objects from functions.

It also illustrates a special syntax whereby the whole of a function body can be a try … catch block.

Circle.hppCircle Encapsulation Specification
/*!@file  Circle.hpp
*  @brief [c++11] Circle Specification
*/
#if !defined _CIRCLE_HPP_
    #define  _CIRCLE_HPP_

#define _USE_MATH_DEFINES       // non-standard, but relatively common,
#include <cmath>                // works in GCC and Visual C++ 2015+.
#if defined M_PI
   constexpr double PI = M_PI;  // needed for `M_PI` & `sqrt()`.
#else
   constexpr double PI = 3.14159265358979323846;
   constexpr double M_PI = PI;
#endif

class Circle {

   private: // data members | fields | instance variables

      double radius_;

   public: // ctors & dtors.

      Circle ()                 // default ctor.
         : radius_{0.0}         // ctor initialising list syntax.
         { }

      Circle (const Circle&)    // let compiler create copy ctor.
         = default;

      explicit Circle (double radius);

   public: // accessors and other methods

      inline double radius () const { return radius_; }
      void set_radius (double radius);

      inline double area () const;
      void set_area (double area) {
         set_radius(sqrt(are / PI));
         }
      inline double circum () const;
      void set_circum (double circum) {
         set_radius(circum / (2.0 * PI));
         }
   };

inline double Circle::area () const {
   return PI * radius_ * radius_;
   }

inline double Circle::circum () const {
   return 2.0 * PI * this->radius_; // `this->` is optional.
   }

extern std::ostream& operator<< (std::ostream&, const Circle&);

#endif // _CIRCLE_HPP_

We show some functions inline inside the class and inline outside the class. Two functions are not inline, and are declared only. They must still be defined in a .cpp source file. We also over­load the in­ser­tion operator for Circle objects. We could have declared it as a friend func­tion in­side the class, but that is only necessary if its implementation required access to pri­vate mem­bers, which this one does not.

Circle.cppCircle Encapsulation Implementation
/*!@file  Circle.cpp
*  @brief [c++11] Circle Implementation
*/
#include <iostream>
#include <iomanip>
#include <stdexcept>

#include "Circle.hpp"

Circle::Circle (double radius) {
   if (radius >= 0.0)
      radius_ = radius;
   else
      throw std::runtime_error("Circle::Circle(double): invalid radius");
   }

void Circle::set_radius (double radius) {
   if (radius >= 0.0)
      radius_ = radius;
   else
      throw std::runtime_error("Circle::set_radius: invalid radius");
   }

// overloaded insertion operator

std::ostream& operator<< (std::ostream& lhs, const Circle& rhs) {

   std::ios::fmtflags org_flags{lhs.flags()};

   lhs << std::fixed << std::setprecision(4);
   lhs << "[R:" << rhs.radius() << ", "
       << "C:"  << rhs.circum() << ", "
       << "A:"  << rhs.area() << "]";

   lhs.flags(org_flags);
   return lhs;
   }

This is a multi-file program, so all the source files must be compiled and linked together, as shown before. Here is an example GNU make Makefile which can be used with GCC. It should be portable, but does assume an rm command is available (only for the clean target, though).

MakefileCircle Encapsulation Make File
###@file  Makefile
#  @brief GNU Make Makefile for Encapsulated Circle Example Program
#
# Call make with: `make all DEBUG=1` for a “debug compile”, or
# call make with: `make all`         for a “release compile”.
#
# Note: If using on Windows with GNU's `make` or `mingw32-make`, add
#       `.exe` to the value for the `PROG` macro below & if you do
#       not have a `rm` command, change the value for `RM` to:
#       `cmd /c del`
#
PROG = circlecalc
SRCS = $(wildcard *.cpp)
OBJS = $(SRCS:.cpp=.o)

# or in Cmd Prompt: `RM = cmd /c del`
RM = rm
CXX = g++
CXXFLAGS = -Wall -Wextra -pedantic -std=c++11
LDFLAGS =

ifdef DEBUG
   CXXFLAGS += -O0 -g
else
   CXXFLAGS += -O2 -DNDEBUG
endif

%.o: %.c
	$(CXX) $(CXXFLAGS) -c -o $@ $<

$(PROG): $(OBJS)
	$(CXX) $(CXXFLAGS) -o $@ $(OBJS) $(LDFLAGS)

.PHONY: all clean

clean:
	@-$(RM) $(PROG) $(OBJS)

all : $(PROG)

If running make does not work, try mingw32-make instead. With the Incus Data packaging of Min­GW-w64 and utilities, you must run the latter, although we have created an alias for you with doskey in the default startup.cmd file, called gmake. This might help stave off car­pal tun­nel syn­drome from too much typing.

The Makefile example above, does not deal with header files, which is very common. But some­times you want files to be recompiled, if one or more headers change. In that case, the generic na­ture much be modified to be more specific. This means that for each .o file, one must not only spec­i­fy the related .cpp file, but also which headers are considered dependencies.

TIPGenerate Dependencies

You can use: g++ -MM *.cpp to create a list of targets and dependencies.


2017-11-18: Update to new admonitions. [brx]
2017-09-22: Edited. [brx;jjc]
2017-03-20: Created. [brx]