Skip to the content of the web site.

Classes

In this topic, we will introduce classes in their most standard form. We will not delve into the details, but rather look at how they are used practically.

Suppose you are dealing with many object you are trying to model; perhaps objects in space, which have positions, velocities and mass. Because all the same objects have all the same properties, you could keep a arrays of all of these properties:

    // An array of 100 entries, where each of the 100 entries is a 3-dimensional array
    std::array<std::array<double, 3>, 100> position;
    std::array<std::array<double, 3>, 100> velocity;
    std::array<double, 100> mass;

Now, if you wanted to, for example, calculate the distance between two such points or the speed of a point, you could write appropriate functions:

double distance( std::array<double, 3> posn1,
                 std::array<double, 3> posn2 ) {
    double sum_of_squares{0.0};

    for ( int k{0}; k < 3; ++k ) {
        sum_of_squares += (posn1[k] - posn2[k])*(posn1[k] - posn2[k]);
    }

    return std::sqrt( sum_of_squares );
}

double speed( std::array<double, 3> velocity ) {
    double sum_of_squares{0.0};

    for ( int k{0}; k < 3; ++k ) {
        sum_of_squares += velocity[k]*velocity[k];
    }

    return std::sqrt( sum_of_squares );
}

and so calculate the distance between the 3rd and 9th objects using and the momentum of the 5th object as:

    std::cout << "Distance between objects 3 and 9: "
              << distance( position[3], position[9] ) << std::endl;
    std::cout << "            Momentum of object 5: "
              << speed( position[5] )*mass[5] << std::endl;

The second is a little awkward, as you must always remember to ensure that your coefficients are the same.

This is also difficult to manage, because all the information about the kth object are stored in three different arrays, and if each object has additional information (such as a unique identifier or a name), that, too must be kept in yet another array.

Another problem is that a programming error may lead to, for example, a mass being set to a negative number, simply by writing:

    mass[7] = -0.543;

Likely, all of your algorithms require that the mass is positive.

Instead, classes are much more useful, because they keep related information about the same object together. For this example, above, you define the class as follows:

#pragma once
#include <string>
#include <array>

// Class declaration
//  - This declares to the compiler that 'Massive_object' is a class
class Massive_object;

// Class definition
//  - This defines the class by listing the member variables
//    (and later, member functions)
class Massive_object {
    public:

    private:
        // Member variable declarations
        std::string name_;
        double mass_;
        std::array<double, 3> position_;
        std::array<double, 3> velocity_;
};

This should be put in a file called Massive_object.hpp.

Each of these name_, mass_, position_ and velocity_ are called member variables.

Now, instead, we can create an array of 100 of these objects:

    array<Massive_object, 100> data;

This allocates memory for 100 objects, each of which has all of these properties.

Now, classes do not allow you to access these values directly; so first to initialize these objects, you must write a constructor:

#include <string>
#include <array>
#include <stdexcept>

// Class declaration
//  - This declares to the compiler that 'Massive_object' is a class
class Massive_object;

// Class definition
//  - This defines the class by listing the member variables
//    (and later, member functions)
class Massive_object {
    public:
        // Member function declarations
	Massive_object( std::string name, double mass );

    private:
        // Member variable declarations
        std::string name_;
        double mass_;
        std::array<double, 3> position_;
        std::array<double, 3> velocity_;
};

// Member function declarations

// The constructor starts by listing all member varaibles and
// initializing all of them, even if it is with the default value.
Massive_object::Massive_object( std::string name, double mass ):
name_{name},
mass_{mass},
position_{},
velocity_{} {
    if ( mass <= 0.0 ) {
        throw std::domain_error( "The mass of '" + name_
                               + "' must be positive, but got "
                               + std::to_string( mass ) );
    }
}

You can now use this to initialize your array:

#include "Massive_object.hpp"
#include 

int main();

int main() {
    std::array<Massive_object, 8> planets{
        Massive_object{"Mercury", 3.285e23},
        Massive_object{"Venus",   4.867e24},
        Massive_object{"Earth",   5.972e24},
        Massive_object{"Mars",    6.390e23},
        Massive_object{"Jupiter", 1.898e27},
        Massive_object{"Saturn",  5.683e26},
        Massive_object{"Uranus",  8.681e25},
        Massive_object{"Neptune", 1.024e26}
    };

    return 0;
}

Accessing the name and mass

If we wanted to access the name and mass, we could write two such functions. Each of these names is prefixed by the word const, meaning constant. This says that by calling this function, it is not allowed to change any of the member variables of the object.

// Class definition
class Massive_object {
    public:
        // Member function declarations
        Massive_object( std::string name, double mass );
	std::string name() const;
	double mass() const;

    private:
        // Member variable declarations
        std::string name_;
        double mass_;
        std::array<double, 3> position_;
        std::array<double, 3> velocity_;
};

// Member function definitions
//  -- skip previously defined member functions

// Access the name
std::string Massive_object::name() const {
    return name_;
}

// Access the mass
double Massive_object::mass() const {
    return mass_;
}

We can now append the following code to our main() function:

    for ( int k{0}; k < 8; ++k ) {
        std::cout << "The mass of " << planets[k].name()
                  << " is "         << planets[k].mass()
                  << " kg"          << std::endl;
    }

Changing the mass

Generally, names do not change, so once an object is declared, it is unlikely any user may want to change the name. A users may, however, want to change the mass; perhaps the system is modelling a collision.

// Class definition
class Massive_object {
    public:
        // Member function declarations
        Massive_object( std::string name, double mass );
	std::string name() const;
	double mass() const;
	void mass( double mass );

    private:
        // Member variable declarations
        std::string name_;
        double mass_;
        std::array<double, 3> position_;
        std::array<double, 3> velocity_;
};

// Member function definitions
//  -- skip previously defined member functions

// Change the mass
//  - ensure that the mass is positive
void Massive_object::mass( double mass ) {
    if ( mass >= 0.0 ) {
        mass_ = mass;
    } else {
        throw std::domain_error( "The mass of '" + name_
                               + "' must be positive, but got "
                               + std::to_string( mass ) );
    }
}

We can now add the following code to main()?

    // Update the mass of Mars:
    planets[3].mass( 6.4171e23 );

    std::cout << "The mass of " << planets[3].name()
              << " is now "     << planets[3].mass()
              << " kg"          << std::endl;

We can now also implement many other member functions that give information about the objects:

In general, many member variables will have corresponding functions either access or modify them.

// Class definition
class Massive_object {
    public:
        // The member functions to retrieve and set the
	// member variable member_name_ have the
	// same name, but no trailing underscore
	typename member_name() const;
	void member_name( typename member_name );

    private:
        // Only the member name is suffixed with an underscore
	typename member_name_;
};

Accessing and updating the position and velocity member variables

The position and velocity are arrays, and thus, we will allow two means of accessing or modifying these member variables:

  • by accessing or modifying the entire array, or
  • by accessing or modifying specific entries.

Thus, we have the following accessors or modifiers:

        std::array<double, 3> position() const;
        double position( int n ) const;
        std::array<double, 3> velocity() const;
        double velocity( int n ) const;

        void position( std::array<double, 3> position );
        void position( int n, double position );
        void velocity( std::array<double, 3> velocity );
        void velocity( int n, double velocity );

We should, however, be careful about the modifiers: if these are real vectors, we should not allow the user to assign any of inf, -inf, or nan. Thus, we will have to make careful checks.

std::array<double, 3> Massive_object::position() const {
    return position_;
}

double Massive_object::position( int n ) const {
    if ( ( n >= 0 ) && ( n < 3 ) ) {
        return position_[n];
    } else {
        throw std::domain_error( "The index must be 0, 1 or 2, but got "
                                + std::to_string( n ) ); 
    }
}

std::array<double, 3> Massive_object::velocity() const {
    return velocity_;
}

double Massive_object::velocity( int n ) const {
    if ( ( n >= 0 ) && ( n < 3 ) ) {
        return velocity_[n];
    } else {
        throw std::domain_error( "The index must be 0, 1 or 2, but got "
                                + std::to_string( n ) ); 
    }
}

void Massive_object::position( std::array<double, 3> position ) {
    for ( int k{0}; k < 3; ++k ) {
        if ( !std::isfinite( position[k] ) ) {
            throw std::domain_error( "A position must be finite, but got "
                                   + std::to_string( position[k] ) );
        }
    }

    position_ = position;
}

void Massive_object::position( int n, double position ) {
    if ( ( n >= 0 ) && ( n < 3 ) ) {
        if ( std::isfinite( position ) ) {
            position_[n] = position;
        } else {
            throw std::domain_error( "A position must be finite, but got "
                                   + std::to_string( position ) );
        }
    } else {
        throw std::domain_error( "The index must be 0, 1 or 2, but got "
                            + std::to_string( n ) ); 
    }
}

void Massive_object::velocity( std::array<double, 3> velocity ) {
    for ( int k{0}; k < 3; ++k ) {
        if ( !std::isfinite( velocity[k] ) ) {
            throw std::domain_error( "A velocity must be finite, but got "
                                   + std::to_string( velocity[k] ) );
        }
    }

    velocity_ = velocity;
}

void Massive_object::velocity( int n, double velocity ) {
    if ( ( n >= 0 ) && ( n < 3 ) ) {
        if ( std::isfinite( velocity ) ) {
            velocity_[n] = velocity;
        } else {
            throw std::domain_error( "A velocity must be finite, but got "
                                   + std::to_string( velocity ) );
        }
    } else {
        throw std::domain_error( "The index must be 0, 1 or 2, but got "
                                + std::to_string( n ) ); 
    }
}

Other member functions

Let us introduce three member functions that describe something about the object.

// Class definition
class Massive_object {
    public:
        // Member function declarations
        // ...constructors and other accessing (const) member functions...

	double speed() const;
	double momentum() const;
	double energy() const;

        // ...other modifying member functions...

    private:
        // Member variable declarations...
};

// Member function definitions
//  -- skip previously defined member functions

double Massive_object::speed() const {
    double sum_of_squares{0.0};

    for ( int k{0}; k < 3; ++k ) {
        sum_of_squares += velocity_[k]*velocity_[k];
    }

    return std::sqrt( sum_of_squares );
}

double Massive_object::momentum() const {
    return mass_*speed();
}

double Massive_object::energy() const {
    double current_speed{ speed() };
    return 0.5*mass_*current_speed*current_speed;
}

You will note that in the momentum() member function, speed() is called, and it is therefore called on the same object that the original momentum function was called on.

Next, let us consider some member functions that describe the relationship between two different objects. For this, we need the user to pass the second object. For example, what is the distance between this and another object, and what is the force between this massive object and another massive object? Also, what is the acceleration of this object towards another object? For this, we must pass the other object as an argument to the member function, but we don't want to change the object, so we will pass these by constant reference. What this does is that when you access the parameter, you are actually accessing the very object that was passed as an argument.

// Class definition
class Massive_object {
    public:
        // Member function declarations
        // ...constructors and other accessing (const) member functions...

	double distance( Massive_object const &other ) const;
	double force( Massive_object const &other ) const;
	std::array<double, 3> acceleration_towards( Massive_object const &other ) const;

        // ...other modifying member functions...

    private:
        // Member variable declarations...
};

// Member function definitions
//  -- skip previously defined member functions

double Massive_object::distance( Massive_object const &other ) {
    double sum_of_squares{0.0};

    for ( int k{0}; k < 3; ++k ) {
	double difference{ position_[k] - other.position_[k] };
        sum_of_squares += difference*difference;
    }

    return std::sqrt( sum_of_squares );
}

// Return the magnitude of the force
double Massive_object::force( Massive_object const &other ) const {
    double r{ distance( other ) };

    return 6.67430e-11*mass()*other.mass()/r/r;
}

//                            _
// _       _        m_1 m_2   r        _
// F = m_1 a_1 = G --------- --- where r is the vector from the
//                      2     r
//                     r
// second object to the first.
std::array<double, 3> acceleration_towards( Massive_object const &other ) const {
    std::array<double, 3> acceleration{};
    double r{0.0};

    for ( int k{0}; k < 3; ++k ) {
        acceleration[k] = other.position_[k] - position_[k];
        r += acceleration[k]*acceleration[k];
    }

    double r3{ r*std::sqrt( r ) };

    for ( int k{0}; k < 3; ++k ) {
        acceleration[k] *= 6.67430e-11*other.mass()/r3;
    }

    return acceleration;
}

Adding a radius

One beauty of a class is that you can make modifications at a later point without breaking anything else. Suppose, for example, we now want to include a radius of the planet.

class Massive_object { public: // Member function declarations Massive_object( std::string name, double mass, double radius = 0.0 ); std::string name() const; double mass() const; double radius() const; // ...other accessing (const) member functions... // ...other modifying member functions... private: // Member variable declarations std::string name_; double mass_; double radius_; std::array<double, 3> position_; std::array<double, 3> velocity_; }; // The constructor starts by listing all member variables and // initializing all of them, even if it is with the default value. Massive_object::Massive_object( std::string name, double mass, double radius ): name_{name}, mass_{mass}, radius_{radius}, position_{}, velocity_{} { if ( mass <= 0.0 ) { throw std::domain_error( "The mass of '" + name_ + "' must be positive, but got " + std::to_string( mass ) ); } if ( radius < 0.0 ) { throw std::domain_error( "The radius of '" + name_ + "' must be positive, but got " + std::to_string( radius ) ); } } // Access the radius double Massive_object::radius() const { return radius_; }

Modelling reality

The next step is use these tools to model reality.