mixpp: Memory Management in BDM

Memory Management in BDM

C++ memory management is notoriously flexible, allowing a wide range of efficient and dangerous techniques. BDM uses conventions which allow high implementation efficiency (not absolutely maximal, but within a measurement error of the most efficient way) while substantially reducing the danger of memory errors. These conventions are described below.

Constructors

  • Each configurable class must have public default constructor (that is, a constructor which takes no parameters). This is required by the configuration framework. Other constructors may also be defined when convenient.
  • Each constructor must initialize all fields of the constructed object, to prevent unpredictable behavior.
One consequence of the points above is the case when a default constructor doesn't have the data with which to initialize a new object - in that case it can simply call the default constructors of all its object fields (and base classes), but must explicitly initialize all numeric fields and raw pointers to 0. Such an object isn't valid as constructed and must have some additional initialization methods (typically from_settings, for reading its configured state), but it can at least be destroyed.

Exceptions

BDM uses exceptions to signal runtime and some logic errors. The library aims to provide the minimal exception safety (that is, throwing an exception doesn't crash and doesn't leak any resources) for all thrown exceptions except memory errors - when a program using BDM exhausts memory, it should be terminated as soon as possible (and in most cases it has probably already terminated by itself). Specific exceptions may provide stronger guarantees, as documented for specific cases. All exceptions thrown out of the library are descendants of std::exception. Since they're organized into a class hierarchy, they should be caught by reference:

try {
        mpdf* mtmp = UI::build<mpdf> (_Sources, i);
        Sources (i) = mtmp;
} catch (UIException &exc) {

This saves one call to copy constructor and prevents slicing.

Pointers

Pointers are used extensively (for efficiency), but usage of raw pointers should be minimized.

Objects allocated by operator new should be assigned to a smart pointer instance immediately upon their construction, so that they can be automatically deleted after use. BDM implements its own reference-counted smart pointer template, bdm::shared_ptr, whose interface and semantics are close to the proposed standard std::tr1:shared_ptr (which is planned to replace bdm::shared_ptr once it becomes widely available). Note that objects allocated on the stack must not have their addresses passed to shared_ptr - that is a bug leading to intermittent runtime errors.

Non-null heap pointers may also be kept in instances of object_ptr, which is convertible to (and from) shared_ptr. The SHAREDPTR macro (or SHAREDPTR2, for templated types) defines standartized names for these instances, simplifying library usage:

/*
 egamma_ptr is typedef for object_ptr<egamma>, whose default
 constructor calls new egamma()
*/
egamma_ptr eG;
eG->set_parameters ( a, b );
epdf_array Coms ( 2 ); // epdf_array is typedef for Array<shared_ptr<epdf> >
Coms ( 0 ) = eG; // object_ptr<T> is derived from shared_ptr<T>
/*
 The egamma instance doesn't leak: if the shared_ptr instance which
 wraps it isn't assigned to anything else, the pointer is deleted by
 the destructor of either object_ptr, or Array, whichever runs last.
*/

Pointers kept in object fields should be wrapped in a shared_ptr instance, which will automatically keep them valid (at least) for the lifetime of the containing object. When that isn't possible, it should be documented why their containing class doesn't delete them and who does.

Pointers passed as arguments into functions (and methods) are generally not expected to stay valid after the function returns - when that is required, the parameter's documentation should specify the required scope:

/* the pointer must stay valid for the lifetime of the object */
CurrentContext ( const char *name, int idx );

A simpler alternative is just to pass a shared pointer:

class mratio: public pdf {
protected:
        shared_ptr<epdf> den;
public:
        mratio ( const epdf* nom0, const RV &rv, bool copy = false ) {
                // build denominator
                den = nom0->marginal ( rvc );

In the case above, passing a (constant) reference to shared_ptr<epdf> might be more efficient, but no measurements have been performed.

Functions returning raw pointers should document the scope of their validity:

/*
 Returns the stored pointer (which remains owned by this
 instance).
*/
T *get();

Functions generally shouldn't return raw pointers allocated by operator new - such pointers should be wrapped in an instance of shared_ptr, so that the pointer's unlimited life expectancy is encoded in the function signature:

virtual shared_ptr<epdf> marginal (const RV &rv) const;

Generated on 2 Dec 2013 for mixpp by  doxygen 1.4.7