Extending our Solar System Class#
Let’s make our class more useful. Lets implement the following functions:
get_planet(name): This will take the name of a planet and return thePlanetobject.We have a choice of how we want to do this.
We can return a pointer to the
Planetin theplanetsvector. This would look like:Planet* get_planet(const std::string& name);
This will take the name of a planet, and if it exists in our
planetsvector, it will return a pointer to thePlanet. If the planet does not exist, then the pointer will benullptr.We can return a reference to the
Planetin theplanetsvector. This would look like:Planet& get_planet(const std::string& name);
This is similar to the pointer version, but we need to deal with the case where the planet is not found. There is no
nullptrequivalent for references—a reference must always refer to a valid object.Additionally, we cannot just return an empty
Planetobject, i.e.,Planet{}, because that would be a local / temporary object that will go out of scope once the function exits (and therefore be destroyed).Our solution here is to abort, using
std::exit().We can return a copy of the
Planetin theplanetsvector. This would look like:Planet get_planet(const std::string& name);
Now we could use an empty
Planetfor the case where the name is not found, and the calling function can just check if thep.name.empty()for a return valuepto determine if no planet was found.
The first two approaches (returning
Planet*andPlanet&) would allow a user to directly modify the data in theplanetsvector via the return value. This may not be what we want. In that case, we can add theconstqualifier to the return type in the function definition to ensure that it cannot be used to modify the data.Note
We’ll take the second approach here (a reference), but we’ll make it
const. I’ll also include an implementation that uses the pointer separately so you can compare, if desired.get_period(name): This will take the name of the planet and compute its period using Kepler’s laws. If the planet doesn’t exist, we’ll have it return-1. This function will look like:double get_period(const std::string& name);
Implementation#
Here’s our updated class:
#ifndef SOLAR_SYSTEM_H
#define SOLAR_SYSTEM_H
#include <vector>
#include <cassert>
#include <cmath>
#include <algorithm>
#include <iostream>
#include <string>
#include <cstdlib>
#include "planet.H"
struct SolarSystem {
// member data
double star_mass;
std::vector<Planet> planets;
// our constructor
SolarSystem(const double mass)
: star_mass{mass}
{
assert(mass > 0.0);
}
// member functions
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 (std::ranges::any_of(planets,
[&] (const Planet& p)
{return p.name == 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;
}
}
const Planet& get_planet(const std::string& name) {
auto it = std::ranges::find_if(planets,
[&] (const Planet& p)
{return p.name == name;});
// if the planet doesn't exist, the iterator will point to
// .end()
if (it == planets.end()) {
std::cout << "planet not found";
std::exit(1);
}
return *it;
}
double get_period(const std::string& name) {
double period{-1.0};
const auto &p = get_planet(name);
period = std::sqrt(std::pow(p.a, 3.0) / star_mass);
return period;
}
};
#endif
Some notes:
We’ve rewritten our
add_planetcheck on whether the planet already exists to use std::ranges::any_of. This will returntrueif we already have a planet with that name. This makes the code more compact.We use a lambda-function here. The capture clause,
[&]will capturenameby reference from the surrounding scope so it can be used in our function body.Our
get_planetfunction uses std::ranges::find_if to find the planet with the name we provide. This returns an iterator that points to the planet. If the planet does not exist, then the iterator will point to one-past the last element,planets.end().We want to return a reference to the
Planetin the vector, and not the iterator, so we simply dereference it,return *it;
Driver#
Here’s a driver using our new implementation:
#include <iostream>
#include "solar_system.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);
ss.print_planets();
std::cout << "period of alpha = "
<< ss.get_period("alpha") << std::endl;
}
try it…
Let’s use our get_planet function to get a reference to one of
the planets, and then print its properties using the <<
operator.
Pointer version#
Here’s a version that implements the get_planet class returning Planet*:
#ifndef SOLAR_SYSTEM_H
#define SOLAR_SYSTEM_H
#include <vector>
#include <cassert>
#include <cmath>
#include <algorithm>
#include <iostream>
#include <string>
#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 (std::ranges::any_of(planets,
[&] (const Planet& p)
{return p.name == 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;
}
}
const Planet* get_planet(const std::string& name) {
auto it = std::ranges::find_if(planets,
[&] (const Planet& p)
{return p.name == name;});
// if the planet doesn't exist, the iterator will point to
// .end()
if (it == planets.end()) {
return nullptr;
}
const Planet& p = *it;
return &p;
}
double get_period(const std::string& name) {
double period{-1.0};
const auto *p = get_planet(name);
if (p) {
period = std::sqrt(std::pow(p->a, 3.0) / star_mass);
}
return period;
}
};
#endif
There are a few bits here that need explanation:
In our
get_planetfunction, we now returnnullptrif our iterator points toplanets.end(). This allows the caller to safely check if the planet was found and avoids the need to dostd::exit.If the planet is found, then we need to convert the iterator into a pointer to a planet and then return the address of this. The important bit, is that the pointer should point to the object as it lives in the vector, and not a local / temporary variable. We do this as:
const Planet& p = *it; return &p;
This first creates a reference to the
Planetthat lives in ourplanetsvector. Then we return the address of that.We could also write this as:
return &(*it);
but the first form is more explicit.
In get period, we get a pointer to our planet as:
const auto *p = get_planet(name);
To access the member data, we need to dereference it. So to get the semi-major axis, we could do:
auto a = (*p).a;
but C++ provides a shortcut for access a member of a pointer to an object—the
->operator (see member access operators), so we can equivalently do:auto a = p->a;