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 using 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 framework, it does not care how you organise your code. Here we show how the same, simple, program can be rearranged, and still be the same program, i.e., perform exactly the same tasks. We also include a simple object-oriented organisation, which may be ignored in the short-term, depending on your current experience.
Simplest Version
In this version, all the code for the execution of the program tasks is in the main()
function. It performs all I/O and calculations directly.
simpcirc.cpp
— Simplest 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 functions. 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 functions lower down in the file. For that to work, we still had to declare the functions before main()
.
funccirc.cpp
— Circle 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 functions 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 never ever 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 keeping the declarations in this file, we moved it to our own include file (circfuncs.hpp
). The preprocessor will then replace the #include…
line with the contents of the file.
main.cpp
— Circle 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 reuse in programs. Although not syntactically necessary, we always include the header file declaring the functions in the file defining the functions, so the compiler can check that they match.
circfuncs.cpp
— Circle 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;
}
IMPORTANT — Paired Specification & Implementation Files
We cannot stress enough the importance of including xxx.hpp
into its corresponding xxx.cpp
file. It is not always syntactically necessary, but this is the only way we can ensure that declarations and definitions remain in sync. If there is an unintentional mismatch that is not detected, you will have a very-difficult-to-find bug.
circfuncs.cpp
— Circle 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 compilation unit. The macro name (in this case _CIRCFUNCS_HPP_
) is derived from the file name, to help ensure uniqueness. It is a well-established 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 simple, 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 object files as arguments — this effective means “linking”, since the driver (g++
), will only call the linker, when given only object files as arguments.
IMPORTANT — Do 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 restrict a .cpp
file to one class (sometimes called the implementation), with its corresponding header file (sometimes called the specification), just like the ordinary multi-file organisation above.
main.cpp
— Circle 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. Although not shown, it should be clear that we can have arrays of Circle
objects; pass Circle
objects as parameters; 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.hpp
— Circle 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 overload the insertion operator for Circle
objects. We could have declared it as a friend
function inside the class, but that is only necessary if its implementation required access to private members, which this one does not.
Circle.cpp
— Circle 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).
Makefile
— Circle 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 MinGW-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 carpal tunnel syndrome from too much typing.
The Makefile
example above, does not deal with header files, which is very common. But sometimes you want files to be recompiled, if one or more headers change. In that case, the generic nature much be modified to be more specific. This means that for each .o
file, one must not only specify the related .cpp
file, but also which headers are considered dependencies.
TIP — Generate 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]