Example: Solar System Class#
Let’s build on our vector of planets (from Vector of Structs), but now make a class/struct that holds the data and member functions that know how to operate on that data.
Implementation#
First well define the struct called SolarSystem and write the
constructor:
#ifndef SOLAR_SYSTEM_H
#define SOLAR_SYSTEM_H
#include <vector>
#include <cassert>
#include "planet.H"
struct SolarSystem {
double star_mass;
std::vector<Planet> planets;
SolarSystem(const double mass)
: star_mass{mass}
{
assert(mass > 0.0);
}
void add_planet(const std::string& name,
const double a, const double e=0.0) {
// make sure a planet with that name doesn't already exist
// if the pointer returned here is not null, then the planet exists
for (auto p : planets) {
if (name == p.name) {
std::cout << "planet already exists" << std::endl;
return;
}
}
Planet p{.name=name, .a=a, .e=e};
planets.push_back(p);
}
void print_planets() {
for (const auto& p : planets) {
std::cout << p << std::endl;
}
}
};
#endif
Some notes:
After the constructor name and arguments there is a
:and an initialization:SolarSystem(const double mass) : star_mass{mass} { ...
this is called the initialization list. Any member data specified in that (comma-separated) list is initialized when an object is created as an instance of the class.
Note
In this simple example, we really did not need to use an initialization list, but there are situations when it is desired or even required:
if any of our member data is
constor a reference.if we have other classes as our member data (including things like
std::vector) and initializing them involves some overhead. It can be faster to do the initialization via the initialization list.
See the stack overflow Why should I prefer to use member initialization lists? discussion.
In the constructor body we have an
assert()statement—this will abort the code if it is false. This is a way to add runtime checking to the code to ensure that it is being used properly / matching your assumptions.We implement the member functions directly in the header. They are small, so there doesn’t seem to be a need to break them out into a separate
.cppfile.Our
add_planet()member function checks to see if a planet already exists.
This uses planet.H header from before:
#ifndef PLANET_H
#define PLANET_H
#include <string>
#include <iostream>
#include <format>
struct Planet
{
std::string name{};
double a{}; // semi-major axis
double e{}; // eccentricity
};
std::ostream& operator<< (std::ostream& os, const Planet& p) {
os << std::format("{:12} : ({:6.3f}, {:6.3f})",
p.name, p.a, p.e);
return os;
}
#endif
To instantiate the class, i.e., create a SolarSystem object, we would do:
SolarSystem ss(1.0);
where we pass the constructor arguments at the time we create our object.
Driver#
Finally, we can use our SolarSystem class. Here’s a main() function:
#include <iostream>
#include "solar_system.H"
#include "planet.H"
int main() {
SolarSystem ss(2.0);
ss.add_planet("alpha", 1.0);
ss.add_planet("beta", 1.5, 0.1);
ss.add_planet("gamma", 3.0, 0.24);
// this generates an error
ss.add_planet("alpha", 2.0);
ss.print_planets();
}
Building#
We can compile this using the same general GNUmakefile we
developed previously. We’ll add two features to it:
ALL: test_solar_system
# find all of the source files and header files
SOURCES := $(wildcard *.cpp)
HEADERS := $(wildcard *.H)
# create a list of object files by replacing .cpp with .o
OBJECTS := $(SOURCES:.cpp=.o)
CFLAGS := -Wall -Wextra -Wpedantic -Wshadow -g -std=c++20
# a recipe for making an object file from a .cpp file
# Note: this makes every header file a dependency of every object file,
# which is not ideal, but it is safe.
%.o : %.cpp ${HEADERS}
g++ ${CFLAGS} -c $<
# explicitly write the rule for linking together the executable
test_solar_system: ${OBJECTS}
g++ -o $@ ${OBJECTS}
clean:
rm -f *.o test_solar_system
The first is a new target: clean. By doing
make clean
We will remove all of the object files (*.o) and the executable.
The second new feature is the addition of some compilation flags:
CFLAGS := -Wall -Wextra -Wpedantic -Wshadow -g -std=c++20
We saw these in our Making the Compiler Do the Work discussion.
The main advantage to using a class here is that we don’t need to know
how the planet data is actually stored (in this case in a
std::vector<Planet>). By creating member functions that are part
of the class, we hide the implementation details from the user.
Instead they are given a simple set of functions to interact with the
data.