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

From OrbiterWiki
Jump to navigation Jump to search
Line 262: Line 262:
 
</pre>
 
</pre>
  
Note that because there is no overhead, resulting from allocating potentially unused pens, only this 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.
+
Note that because there is no overhead, resulting from allocating potentially unused pens, only this 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.
 
{{Stub}}
 
{{Stub}}

Revision as of 09:04, 22 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), 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. Consider the following counter-example:

void SomeClass::SomeMethod(OBJHANDLE planet, char * baseName)
{
  OBJHANDLE base;
  // ...
  // Other C-like declarations.
  // ...
  {
    base = oapiGetBaseByName(planet, baseName); // Initialising base later.
    // ...
    // Do something with base. Is it initialised? 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); // Initialising upon declaration
    // ...
    // Do something with baseOuter. Is it initialised? No question, since we follow RAII.
    // ...
    {
       OBJHANDLE baseInner = oapiGetBaseByName(planet, "Cape Canaveral"); // Initialising upon declaration. Using a different variable name.
       // ...
       // Do something with baseInner. We're sure that it's initialised 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 * Pens::GetPen() const
{
    return pen;
}

Note that because there is no overhead, resulting from allocating potentially unused pens, only this 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.

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