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

From OrbiterWiki
Jump to navigation Jump to search
(RAII)
Line 20: Line 20:
  
 
==Practical advices for designing classes==
 
==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 behavior (without the need of setting additional arameters 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.
 
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 behavior (without the need of setting additional arameters 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 shoud 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.
 
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 shoud 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.
Line 25: Line 26:
  
 
==Memory management in C++==
 
==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:
 
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:
 
<code>
 
<code>
Line 41: Line 43:
 
</code>
 
</code>
  
The destructor mechanism in C++ allows to facilitate [http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization 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.
+
==Resource Acquisition Is Initialization==
 +
 
 +
The destructor mechanism in C++ allows to facilitate [http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization 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 case, we were able to wrap the memory management inside a class. There are some situations when its impractical, like when initializing pointers. Consider the follwing counter-example:
 +
 
 +
<code>
 +
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, effectovely distracting yourself from your main task
 +
    // ...
 +
    {
 +
      base = oapiGetBaseByName(planet, "Cape Canaveral"); // 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.
 +
      // ...
 +
      // 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.
 +
</code>
 +
 
 +
Proper example:
 +
<code>
 +
void SomeClass::SomeMethod(OBJHANDLE planet, char * baseName)
 +
{
 +
  // ...
 +
  {
 +
    OBJHANDLE baseOuter = oapiGetBaseByName(planet, baseName); // Initialising upon declaration
 +
    // ...
 +
    // Do something with baseOuter. Is it initialised? No question.
 +
    // ...
 +
    {
 +
      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 first base.
 +
      // ...
 +
    } // baseInner gets deallocated.
 +
    // You can still do something with the baseOuter, since it hasn't left the scope.
 +
  } // baseOuter gets deallocated.
 +
}
 +
</code>
 
{{Stub}}
 
{{Stub}}

Revision as of 05:41, 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 behavior (without the need of setting additional arameters 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 shoud 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 shoud 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

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 case, we were able to wrap the memory management inside a class. There are some situations when its impractical, like when initializing pointers. Consider the follwing 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, effectovely distracting yourself from your main task
   // ...
   {
      base = oapiGetBaseByName(planet, "Cape Canaveral"); // 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.
      // ...
      // 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.
   // ...
   {
      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 first base.
      // ...
   } // baseInner gets deallocated.
   // You can still do something with the baseOuter, since it hasn't left the scope.
 } // baseOuter gets deallocated.

}

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