Difference between revisions of "Developing modules in C++ (basic)"

From OrbiterWiki
Jump to navigation Jump to search
m (→‎Getters & setters: - better explanation at GraphicsTeam())
Line 296: Line 296:
 
class GeneralShipData
 
class GeneralShipData
 
{
 
{
  public:
+
    public:
  // These parameters can't be modified directly by outside classes, because no non-const getter is available here:
+
      // these parameters can't be modified by outside classes
  const MovementParameters & GetMovParams() const { return m_movParams; }
+
      const MovementParameters & GetMovParams() const { return m_movParams; }
  void Update() // This can be called only by owners of non-const reference to object of this class
+
      void Update() // This can be called only by owners of non-const reference to object of this class
  {
+
      {
      double dt = 1;
+
          double dt = 1;
      m_movParams.Update( dt ); // The owner can do whatever he wants with its component ...
+
          m_movParams.Update( dt );
       // m_movParams.vx += 0.2; // even directly updating individual variables, but nobody else can!
+
       }
  }
 
  
  private:
+
    private:
  MovementParameters m_movParams; // the member of interest
+
        MovementParameters m_movParams; // the member of interest
  // ...  
+
        // ...
  // other objects
+
        // other objects
  // ...
+
        // ...
 
};
 
};
  
Line 321: Line 320:
 
{
 
{
 
     // gsd.Update(); // sabotage by graphical team unsuccessful - compilation error
 
     // gsd.Update(); // sabotage by graphical team unsuccessful - compilation error
     const MovementParameters & movPar = gsd.GetMovParams(); // but I can still display graphics!
+
     const MovementParameters & movPar = gsd.GetMovParams(); // This is all I can do
 +
    // movPar.x = 3; // Missed again - compilation error
 +
    // but I can still display graphics!
 
     std::cout << "x = " << movPar.x << ", y = " << movPar.y << std::endl;
 
     std::cout << "x = " << movPar.x << ", y = " << movPar.y << std::endl;
 
}
 
}

Revision as of 16:38, 30 July 2012

This article explains a few basic concepts about planning add-on modules in C++. This article is not recommended for readers who have no experience yet in C++.

What this article is not about

It is not goal to describe the C++ language or the standard libraries of C++. It also is not about the OrbiterSDK. Goal is to explain more advanced topics about object-orientation and software architecture. It is no law, no set of rules that have to be followed at all costs. You need a blob class? It could be useful maybe.

But always remember: C++ does only do, what you tell C++ to do.

Classes and objects

The most important capability of C++ is called object-orientation. It simply means, that instead of having data (variables, constants) and instructions functionally separated, both concepts are joined into classes and objects of such classes. An object generally contains logically connected data and functions (here: methods) for manipulating this contained data. One other important is the ability to inherit traits from a superclass to a derived class and overload functions of this superclass with your own versions of the function. This ability is used in many different ways, mostly for generic, abstract programming and code reuse.

Interface classes

An interface class is essentially a class, that only defines the signatures of its functions. It does not implement these functions. Since any class in C++ can inherit from multiple superclasses, it can also implement multiple interfaces at once. And not only that, you can also join multiple interfaces in one more specialized interface.

Such an interface gets very useful, when you want to use classes in your functions, without having to support the whole hierarchy of classes for one feature. Instead, you just declare that you only want a class that implements such an interface to work with.

For example, you could define an interface for all subsystem classes, that can save their state in the scenario file.

Practical advices for designing classes

A very good rule to follow is that the classes should be as small as possible, and responsible for one thing only, ie. ideally, a class should accept configuration input through constructor(s) that completely defines its behaviour (without the need of setting additional parameters via methods, except maybe methods that let add objects in a polymorphic way), and provide one or just a few methods, which provide access to the data that the class processed (it's the class' interface, just as buttons in an elevator). The user of the class should not be bothered about the internals of the class, just as the elevator's user doesn't need to know about the way the elevator's engine functions, only about the buttons. This is called encapsulation, and it helps in focusing on getting new things done, instead of being distracted by reviewing the same code many times. If something goes wrong in the system that the class is responsible for (elevator engine's malfunctions in some cases), you will know that you only need to look into the one class' implementation, and not any other class (other code), that would distract you. Keeping classes small greatly increases probability of reusing these classes in other places of the system that you're designing, or even other projects, so that is makes sense to create a library of such reusable code. Whenever you catch yourself on repeating some portion of code in your project, you should treat it as a design error, and should organize such code into functions as an optional first step, and then into self-contained, encapsulated classes with their own set of variables and methods. Many designers also advice to compose small, low-level classes into higher and higher level classes, instead of building a hierarchy of classes, using inheritance. Inheritance should be only used if there's a chance that a code can be called in a polymorphic way, like two types of guidance algorithms having a common interface - guidance algorithm (inheritance vs composition). Anyway, always keeping classes small allows you make a final decision later in the process of design and implementation.

Example utility class

This class is a one that is used in many places in LaunchMFD. The MFD has three modes: Standard, Compass and Direct Ascent. Each mode is an encapsulated class, that contains the Pens class (AKA Composition). Whenever a mode is destructed, the Pens object is automatically destructed along with it, deallocating its resources. Because not all modes use all LineStyles, there is a certain overhead of allocating additional pens. But guess what? You shouldn't care about it, because the construction and destruction of the Pens is performed so rearely, that it's much better to wait a negligible amount of time while having a readable and easy to maintain general purpose class, than doing the same thing again and in different places, like manually allocating and deallocating individual pens. What if you forget to perform a pen's deallocation, while you allocate it on each MFD's Update() call?. The Pens class has a simple interface, that consists of a pens getter, which is what you should be aiming for, and additionally, an empty default constructor that does the job of pens allocation behing the scenes.

Declaration:

#ifndef PENS_H
#define PENS_H

#include <orbitersdk.h>

class Pens
{
    public:
        Pens();
        virtual ~Pens();

        // Use this enum select the pen that you want with a call like:
        // Pens::GetPen( Pens::Green );
        enum LineStyle
        {
            Green,
            Yellow,
            GreenDashed,
            White,
            Grey,
            Red
        };

        // From now on, type MyPEN means a pointer to Pen in namespace oapi
        typedef oapi::Pen * MyPEN;

        // Class' interface - returns a constant reference to MyPEN
        // You can select the pen you want from the LineStyle
        const MyPEN & GetPen(LineStyle style) const;
    protected:
    private:
        // Declaring copy constructor and assignment operator private makes the class non-copyable
        // This takes away the pain of manual memory management. If you need the class again,
        // just declare it again.
        Pens(const Pens & other);
        Pens & operator=(const Pens & other);

        const int cNumPens; // keep this number in order by counting the number of LineStyle colors
        MyPEN * pens; // will be an array of pens. Could be a std::vector<MyPEN>
};

#endif // PENS_H

Implementation:

#include "Pens.h"
#include <stdexcept>

Pens::Pens()
: cNumPens(6)
{
    pens = new MyPEN[cNumPens]; // allocate storage of pens

    // Allocate pens themselves
    pens[Green]	     = oapiCreatePen(1, 1, GREEN);
    pens[Yellow]     = oapiCreatePen(1, 1, YELLOW);
    pens[GreenDashed]= oapiCreatePen(2, 1, GREEN);
    pens[White]      = oapiCreatePen(1, 1, WHITE);
    pens[Grey]		 = oapiCreatePen(1, 1, GREY);
    pens[Red]		 = oapiCreatePen(1, 1, RED);
}

// Destructor
// Guaranteed to be executed when class leaves scope
Pens::~Pens()
{
    // Deallocate pens
    for(int i = 0; i < cNumPens; ++i)
    {
        oapiReleasePen(pens[i]);
    }
    delete pens; // Deallocate pens storage array
}

const MyPEN & Pens::GetPen(LineStyle style) const
{
    if ( style >= cNumPens )
    {
        // Whoops! You forgot to increment cNumPens after adding a new LineStyle.
        // Must halt the application.
        throw std::invalid_argument("Pens::GetPen(): out of bounds");
    }
    return pens[style];
}

Memory management in C++

A very unique feature of C++ is the warranty that whenever class declared inside a scope (defined as '{' and '}') leaves this scope, its destructor is always called, allowing you to perform optional cleanups of your code. After the destructor has been called, the class is deallocated, freeing its memory. This feature lets your application handle memory, or generally: resources, in a predictable way, freeing them, when their user (the class) doesn't need them anymore. Therefore, if you encounter a situation where you have a double scope and need a class only in the second scope, declare it in the second scope, not at the beginning of the function, so that resources taken by the class are released as soon as possible, for example:

void SomeClass::SomeMethod()
{
  // ...
  // UtilClass util; // wrong place to declare UtilClass;
  // ...
  {
     UtilClass util; // correct place to declare UtilClass
     util.DoSth(); // since we use it only in this scope
  }  // Releasing util's resources by automatically calling its destructor
  // ...
  // ...
} // Otherwise would release them possibly much later

Resource Acquisition Is Initialization

The destructor mechanism in C++ allows to facilitate RAII. In the example Pens class, you are guaranteed, that upon the class' declaration, all the resources are allocated and the class is instantly ready to use. On the other hand, when the class leaves the scope, so when you won't need it, its resources are guaranteed to be deallocated, and you don't need to do it by hand. In our Pens example, we were able to wrap the memory management inside a class. There are some situations when its impractical, like when initializing pointers, that don't need to be released, but following RAII still increases code readability. Consider the following counter-example:

void SomeClass::SomeMethod(OBJHANDLE planet, char * baseName)
{
  OBJHANDLE base;
  // ...
  // Other C-like declarations.
  // ...
  {
    base = oapiGetBaseByName(planet, baseName); // Initializing base later.
    // ...
    // Do something with base. Is it initialized? Yes, but you have to double check it,
    // effectively distracting yourself from your main task
    // ...
    {
       // Get another base into the same variable,
       // theoretically releasing resources of the first OBJHANDLE, 
       // but it's a poor trade-off of resource management vs code readability.
       base = oapiGetBaseByName(planet, "Cape Canaveral"); 
       // ...
       // Do something with base, but is it the first base or the second base? You have to check it, distracting yourself again. 
       // ...
    }
    // What if we needed the first base here again, and the variable points to the second base?
  }
} // Releasing resources of the second base.

Proper example:

void SomeClass::SomeMethod(OBJHANDLE planet, char * baseName)
{
  // ...
  {
    OBJHANDLE baseOuter = oapiGetBaseByName(planet, baseName); // Initializing upon declaration
    // ...
    // Do something with baseOuter. Is it initialised? No question, since we follow RAII.
    // ...
    {
       OBJHANDLE baseInner = oapiGetBaseByName(planet, "Cape Canaveral"); // Initializing upon declaration. Using a different variable name.
       // ...
       // Do something with baseInner. We're sure that it's initialized and that it isn't the outer base.
       // ...
    } // baseInner gets deallocated.
    // You can still do something with the baseOuter, since it hasn't left the scope.
  } // baseOuter gets deallocated.
}

Following our Pens example, you could also stick more into RAII, and create a RAIIPen class, that wouldn't hold all typical pens you'd ever use, but create them individually on demand and deallocate them instantly, and not during MFD modes reconstruction. You would tell the class which pen you want through its constructor, by passing an enum, similarly to the Factory design pattern:

#ifndef RAIIPEN
#define RAIIPEN

#include <orbitersdk.h>

class RAIIPen
{
    public:
       enum LineStyle
        {
            Green,
            Yellow,
            GreenDashed,
            White,
            Grey,
            Red
        };
        RAIIPen( LineStyle style );
        virtual ~RAIIPen();

        const oapi::Pen * GetPen() const;
    protected:
    private:
        RAIIPen(const RAIIPen & other);
        RAIIPen & operator=(const RAIIPen & other);

        oapi::Pen * pen; // oapi Pen
};
#endif

Implementation:

#include "RAIIPen.h"

RAIIPen::RAIIPen( LineStyle style )
{
  pen = NULL;
  switch ( style )
  {
    case Green:
      pen = oapiCreatePen(1, 1, GREEN);
    break;
    case Yellow:
      pen = oapiCreatePen(1, 1, YELLOW);
    break;
    case GreenDashed:
      pen = oapiCreatePen(2, 1, GREEN);
    break;
    case White:
      pen = oapiCreatePen(1, 1, WHITE);
    break;
    case Grey:
      pen = oapiCreatePen(1, 1, GREY);
    break;
    case Red:
      pen = oapiCreatePen(1, 1, RED);
    break;  
    // A compiler warning will be generated if one defined colour isn't serviced. The warning shouldn't be ignored.
  }
}

RAIIPen::~RAIIPen()
{
    oapiReleasePen(pen);  // Deallocate pen
}

const oapi::Pen * RAIIPen::GetPen() const
{
    return pen;
}

Composition vs inline declaration

Note that because there is no overhead, resulting from allocating potentially unused pens, only the RAIIPen approach promotes declaring a new pen object in a scope as deep as possible, instead of composing the Pens class in another, higher level class. However, because the resources are supposed to be created and released multiple times, because Update() method is called frequently, the above example may be considered over-zealous. Nevertheless, the exact impact on performance must be measured using a profiler, before making final and autoritative decisions, and never guessed, unless some really obvious performance mistake is made during allocation and deallocation. An obvious case when you absolutely must compose a class into another is when the component is supposed to hold state in a variable, like time passed after launch.

Const correctness

Const correctness (CC) deals with deliberate limitation of write access to certain classes or variables, providing a mechanism of locking these variables and exposing them as read only to other parts of your code or other team, that both aren't supposed to modify these data. Imagine a situation where you have a team of experienced physical core programmers, and possibly a team of inexperienced programmers of simple graphics. You would want the graphical team to only read the physical data for display, and never modify them, to avoid hard to track bugs. This can be achieved by passing the objects of interest as const references to original objects, or eventually as copies of these objects, if you can afford copying large chunks of data. The CC works in a recursive way, meaning that if you pass a const reference to a physics engine to the graphical team, they will only be able to access const methods of the engine, which can provide only const access to the engine components, be it other objects, or variables. Even if you program alone, using CC you can make your application more rock solid, and less error prone. Of course, the physics engine should be able to provide read/write access to its components via other, non-const methods, which can be called only from classes which receive a non-const reference to physics engine.

Getters & setters

When designing encapsulated classes, whose variables are accessed only through the class' methods, you might get an idea that you can never make any variable public, and that each variable should be accessed through const getters for read access, and non-const setters for write access. This is a high form of purism and abuse of getters and setters will lead to a lot of boilerplate code and will slow down development. Thanks to const correctness in C++, you can sometimes safely define classes with public variables, and share them with other classes as const references. The other classes will never be able to modify the public variables, therefore no individual getters and setters are needed, for example:

#include <iostream>
#include <vector>

class MovementParameters
{
    public:
        MovementParameters() : x(0), y(0), vx(0.5), vy(0.2) // initialization list
        {}
        double x, y; // public members for easy access without 4 pairs of getters and setters
        double vx, vy;
        void Update( double dt ) // This can be called only by owners of non-const reference
        {
            x += vx * dt;
            y += vy * dt;
        }

    private:

};

class GeneralShipData
{
    public:
      // these parameters can't be modified by outside classes
      const MovementParameters & GetMovParams() const { return m_movParams; }
      void Update() // This can be called only by owners of non-const reference to object of this class
      {
          double dt = 1;
          m_movParams.Update( dt );
      }

    private:
        MovementParameters m_movParams; // the member of interest
        // ...
        // other objects
        // ...
};

void PhysicsTeam( GeneralShipData & gsd )
{
    gsd.Update(); // Non-const reference lets me call the non-const Update method
}

void GraphicsTeam( const GeneralShipData & gsd )
{
    // gsd.Update(); // sabotage by graphical team unsuccessful - compilation error
    const MovementParameters & movPar = gsd.GetMovParams(); // This is all I can do
    // movPar.x = 3; // Missed again - compilation error
    // but I can still display graphics!
    std::cout << "x = " << movPar.x << ", y = " << movPar.y << std::endl;
}

int main()
{
    GeneralShipData gsd;
    PhysicsTeam( gsd );
    GraphicsTeam( gsd );
    return 0;
}

Further reading


This article is a stub. You can help Orbiterwiki by expanding it.