Extending our Solar System Class

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 the Planet object.

    We have a choice of how we want to do this.

    • We can return a pointer to the Planet in the planets vector. 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 planets vector, it will return a pointer to the Planet. If the planet does not exist, then the pointer will be nullptr.

    • We can return a reference to the Planet in the planets vector. 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 nullptr equivalent for references—a reference must always refer to a valid object.

      Additionally, we cannot just return an empty Planet object, 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 Planet in the planets vector. This would look like:

      Planet get_planet(const std::string& name);
      

      Now we could use an empty Planet for the case where the name is not found, and the calling function can just check if the p.name.empty() for a return value p to determine if no planet was found.

    The first two approaches (returning Planet* and Planet&) would allow a user to directly modify the data in the planets vector via the return value. This may not be what we want. In that case, we can add the const qualifier 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:

Listing 117 new solar_system.H#
#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_planet check on whether the planet already exists to use std::ranges::any_of. This will return true if we already have a planet with that name. This makes the code more compact.

    We use a lambda-function here. The capture clause, [&] will capture name by reference from the surrounding scope so it can be used in our function body.

  • Our get_planet function 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 Planet in the vector, and not the iterator, so we simply dereference it,

    return *it;
    

Driver#

Here’s a driver using our new implementation:

Listing 118 new test_solar_system.cpp#
#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*:

Listing 119 new solar_system.H#
#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_planet function, we now return nullptr if our iterator points to planets.end(). This allows the caller to safely check if the planet was found and avoids the need to do std::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 Planet that lives in our planets vector. 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;