The x_ptr<> cross-module handles

The x_ptr<> is a cross-module typed object handle, also known as a smart pointer. It should be used in function and method signatures whenever a MMXX-enabled class instance is to be identified through a pointer, that is to say, x_ptr<A> should be used in lieu of A*. Note that x_ptr<> cannot be used with a non-MMXX-enabled class as its parameter. However, it's possible to obtain a base pointer x_ptr<A> to an instance of B if B derives from A even if B is not itself an MMXX-enabled class but A is.

x_ptr<> performs two functions. It provides type-safety for the pointer in question, and enables automatic translations between the module-specific pointers as seen by the compiler. Consider a method foo():

// callee:
void foo(A *obj) {

// caller:
A *p = new A;

where A is a MMXX-enabled class. Because of the use of shadow classes and multiple compiler instances, every module is to use a different (literal) value of A* for the object we've just created. If in the example above, the foo() method were implemented in the same module that invokes it, then the value of A* would be the same across caller and callee and indeed no translation would in fact be necessary. However, if foo() were to be invoked by a different module than the one it's implemented in, then p (the pointer local to the caller) and obj (the pointer local to the callee) would have to differ - if a conversion isn't used, the program will crash. The solution is to use an x_ptr<A> in the signature instead:

// callee:
void foo(x_ptr<A> xobj) {
A *obj = xobj;
// dereference

// caller:
A *p = new A;

which will work whether caller and callee are located in the same module or not. This is possible because the x_ptr<A> class provides automatic conversions back and forth from A*. If you're not familiar with C++'s user-defined automatic type conversions, it might be interesting to know that the A* => x_ptr<A> conversion is implemented through an overloaded constructor x_ptr<A>::x_ptr<A>(A*) while the x_ptr<A> => A* conversion is implemented through a casting operator x_ptr<A>::operator A*(). As part of the declaration of class A, MMXX also overloads the unary A::operator &() (and its const version) to return x_ptr<A> rather than A*. One of the beauties of C++ is that all these conversions will be applied implicitly.

Notice that in the revised callee implementation, we've explicitly dereferenced the x_ptr<A> xobj parameter into a classic A *obj before accessing the object. This is in fact optional: the callee could have just used xobj to invoke bar() directly, i.e. by typing xobj->bar(). There is really no problem with this other than a possible performance penalty: to invoke a method on the object, the actual A* must be extracted from the x_ptr<A> first. As long as the implementation in question is as simple as foo() above and only takes one shot at dereferencing the x_ptr<>, then there is virtually no difference in execution speed. But suppose that foo() invoked bar() several times within a loop, or that it invoked several methods through xobj rather than just bar(). In that case, the performance penalty could conceivably become very significant, and dereferencing into obj first becomes a very good idea.

Thanks to automatic conversions, x_ptr<A> can be used in very much the same way as A* itself. For one, C++ will allow the same types of base/derived class pointer conversions: if B is a subclass of A, then an x_ptr<B> can be assigned to an x_ptr<A> while, just as with pointers, the reverse isn't true (without a dynamic cast.) Similarly, an x_ptr<A> can be compared with another x_ptr<A> for equality. Again, C++ will be using the conversion to A* (and to B* in the first case) to implement these constructs behind the scenes (which again implies that there is a possible performance penalty involved if these constructs are used repeatedly or in loops, which can be eliminated by first dereferencing the pointers explicitly.)

Like an A*, an x_ptr<A> can be null, for instance if a null A* is assigned to it. It can be tested for non-nullness by simply using it in boolean constructs, for instance foo() above could have explicitly tested it by using if (xobj) {...}. Again, this type of usage rests on the A* conversion, an alternative to which is to use the _IsNull() method instead, as in if (!xobj._IsNull()) {...}. Assigning a null value to an x_ptr<A> is a little trickier, in that the actual type of the literal 0 is int and the actual type of the NULL macro is either int or void *, and neither int nor void * can be converted to an x_ptr<A>. The alternative is to make it clear that you're assigning a null A* pointer instead, i.e. by typing xobj = (A*)0; or, in invocations, foo((A*)0).

Aside from _IsNull(), there are three more x_ptr<> methods that are occasionally useful. The _GetMO() method and its const version _GetCMO() can be used respectively to extract an MMXXMetaobject or MMXXConstMetaobject generic object identifier (see the section on MMXXMetaobject in the metaclass interface to learn more about this.) The _Localize() method can be used to make the x_ptr<> "local" to the current module. All this means in practice is that the conversion to A* in the current module may become faster from now on. Note that this in no way corrupts or inhibits further use of the x_ptr<> - it just so happens that its internal representation maintains a notion of the "current" module, and user code is by no means required to worry about this.

Relatives of x_ptr<>: x_cptr<>, x_ref<> and x_cref<>

If the object pointer in question is to a const object, i.e. const A* rather than A*, then x_cptr<A> should be used instead of x_ptr<A>. In particular, notice that x_ptr<const A> will not work, and that const x_ptr<A> means "a const cross-module pointer to a non-const A object" or in other words behaves parallelly to an A* const. The behavior of x_cptr<> is exactly identical to that of x_ptr<> with the exception of the const attribute: an x_cptr<A> will automatically convert to and from a const A*, it is possible to assign an x_ptr<A> to an x_cptr<A> but not the reverse, and so on. The _IsNull(), _Localize() and _GetCMO() methods are identical (but the _GetMO() method is not available.)

The cross-module x_ref<> and x_cref<> are intended to mimic the behavior of object references, that is to say A& and const A& respectively. Here's a revised version of the example above using references instead of pointers:

// callee:
void foo(x_ref<A> xobjref) {
A &objref = xobjref;
// must dereference;

// caller:
A *p = new A;

The primary syntactical difference with regular references is that, unlike in the A* / x_ptr<A> parallel, it's not possible to automatically dereference an x_ref<> to invoke methods on the object (because C++ does not allow overloading of the member operator, e.g. "operator ."). Either the x_ref<A> must be dereferenced explicitly into an A& as in the example above, or the _R() member method must be used, as in xobjref._R().bar(). No syntactical changes are necessary to assign object references to an x_ref<> or x_cref<>, however care must be used to prevent accidentally constructing references to temporary variables (that might be destroyed while the references to them are still outstanding.)

Like regular references, x_ref<> and x_cref<> are never supposed to be "null." However the _IsNull() method is provided for consistency with x_ptr<> and x_cptr<>, and it's indeed possible to "manufacture" a null reference (as it is under many C++ implementations.) Also provided are _Localize(), _GetMO() and _GetCMO() methods with the same semantics.

Like x_ptr<>, none of these types can be used with non-MMXX-enabled classes as their parameters.

Using x_auto_ptr<>

x_auto_ptr<> is an MMXX-enabled version of the standard library's auto_ptr<> class. The primary differences are that an x_auto_ptr<A> refers to its owned object through an x_ptr<A> rather than an A*, and that the owned object is destroyed by way of the VirtualDelete() method rather than by using delete. The same operators are overloaded, and get() and release() methods are compatibly provided. x_auto_ptr<> should be used wherever you would normally use auto_ptr<> but the objects in questions are instances of MMXX-enabled classes - it's important not to use regular auto_ptr<>'s for such objects (primarily because of its use of delete.) Note also that like x_ptr<>, x_auto_ptr<> cannot be used with a non-MMXX-enabled class as its parameter.

Like auto_ptr<>, x_auto_ptr<> is useful for the "resource acquisition is initialization" tecnique as described by Stroustrup, as well as to avoid memory leaks in certain circumstances. Occasionally, x_auto_ptr<> is also useful in conjunction with the functor interface.

MMXX's Exception Model

It is possible to throw exceptions across MMXX module boundaries. It's also possible to have non-fragile user-defined exception types. However, the rules of MMXX's exception model must be respected. There are two exception-related class hierarchies:

  1. std::exception hierarchy (inlined):

  2. MMXXException hierarchy (MMXX-enabled):

Here are the rules:
  1. Always derive any custom exception types from the MMXXException hierarchy.
  2. You may throw and catch instances of the exception classes in the std::exception hierarchy across module boundaries.
  3. You may throw and catch pointers to instances of exception classes you derive from MMXXException across module boundaries.
Derivation from MMXXException is identical to derivation from any other MMXX-enabled class. To throw an MMXXException or an instance of your own MMXXException subclass, you must throw it as a pointer, for example by writing:

throw new MyMMXXExceptionSubclass;

Note how actual pointers (not x_ptr<>'s) must be thrown. This allows catch clauses to take advantage of inheritance in order to differentiate among different MMXXException subclasses - for instance, a single catch (MMXXException *p) will match pointers to instances of all MMXXException subclasses, while a more specific catch (MyMMXXExceptionSubclass *q) will only match pointers to instances of MyMMXXExceptionSubclass and derivates. Cascading exception handlers in order of further generalization (e.g. with MMXXException* last) allows the same level of control otherwise possible when actual instances are thrown. Note that since standard library exceptions can also be thrown across module boundaries, try/catch blocks will typically also include a catch (std::exception &e), perhaps preceded by a catch (std::bad_alloc&) and so on.

Clearly, since all user-defined exception types must derive from MMXXException, exception types are effectively restricted to taking part in a single-inheritance derivation tree with MMXXException at the root (virtual derivation is not an option either, since MMXX does not support it.) However, MMXXException subclasses may derive from other classes as well.

It's also important to keep in mind that since pointers are thrown, the pointed-to exception object is not deleted automatically at the end of the exception handler. This issue can be addressed in two ways: either the exception handler can explicitly invoke the VirtualDelete() method on the exception object once it's done examining it, or it can rely on the garbage-collection feature of the MMXXException class to delete the object later. The garbage-collection feature is implemented by way of a static buffer of pointers, and is enabled by default. If an instance of MMXXException or subclass is created when the number of stale exception objects in the buffer is at or above the current high-water mark, the oldest such object(s) are deleted. The high-water mark can be changed by the user, and individual exceptions can be detached from the garbage collector without deleting them if the user wishes to preserve them for some reason. If desired, the garbage collection feature can be disabled altogether, in which case the user must beware of any leaks that may occur, such as when a MMXXException* is caught by a catch (...). Note that all instances of MMXXException and subclasses count for the purposes of garbage-collection, not just the ones that are actually thrown.

What happens if the module where the exception is caught has no knowledge of the particular user-defined MMXXException subclass whose instance pointer crossed the module boundary? Everything works correctly because MMXX derivation is employed. The code in the module will be able to identify the object by the most-specific base type that it does have knowledge of, or as a straightforward MMXXException* as a last resort.

Note that this arrangement maintains compatibility with code that only throws standard library exceptions, particularly code in the standard library itself. Other code can still violate these rules, e.g. rely on user-defined exception types that do not inherit from MMXXException and are not thrown as pointers. This will not affect the behavior of the program as long as the exception in question does not cross a module boundary or go through an MMXX functor or metastub. If the rule-violating exception does cross such a boundary, the program will not crash, nor - if the user has taken this possibility into account - will this necessarily have negative repercussions. The exception will simply "shapeshift" into an exception of a different type. The exact shapeshifting mechanism is as follows:

Naturally, if the rules are obeyed, no shapeshifting will occur, and the program will not need to be concerned about module boundaries when throwing and catching exceptions.