A Primer on the Interior Pointer

A value type is typically not subject to garbage collection except in two cases: (a) when it is the subject of a box operation, either explicit or implicit, such as,

void f( int ix )

{

    // explicit placement on the CLI heap

    int^ result = gcnew int( -1 );

 

    // exercise result …

 

    // implicit boxing of the integer value ix …

    Console::WriteLine( "{0} :: {1}", ix, result );

};

and (b) when it is contained within a reference type either as a member or as an element of a CLI array. For example,

public enum class Color

     { white, red, orange, yellow, green, blue, indigo, violet };

 

public value class Point

{

    Color m_color;

    float m_x, m_y;

 

    // …

};

 

public ref class Rectangle

{

    Point bottom_left;

    Point top_right;

 

    // …

};

 

void f( Point p )

{

    Rectangle ^hit = gcnew Rectangle( -2, -2, 2, 2 );

    array<int> ^fib = gcnew int(8){ 1,1,2,3,5,8,13,21 };

   

    // …

}

In f(), we allocate two whole objects on the CLI heap, a Rectangle and an array of eight integer elements. The Rectangle object contains two interior value class Point members. These are located on the CLI heap as fixed offsets into the area allocated to the containing Rectangle. Within each Point are two floating point members and a Color member. These are also located on the CLI heap as fixed offsets into the two interior Point objects.

If the Rectangle object is relocated during a sweep of the garbage collector, all its interior members, of course, are relocated as well. The same is true with a CLI array. When the array is relocated, the addresses of each of its elements change as well. We cannot safely assign the address of any interior member or array element to a non-tracking pointer or reference. This isn't just a pitfall that we must learn to recognize and sidestep. Rather, the language disallows it. Our attempt results in a compile-time error. For example,

// error: cannot assign an interior member

// to a non-tracking pointer …

Point *p_bl = hit->BottomLeft;

We need a form of tracking entity to hold the address of an interior member. What are some of the requirements on this entity? Well, one common pattern we felt necessary to support is that of an iterator – in particular when applied to the elements of a CLI array. For example,

void f( array<int> ^fib )

{

    SomeTrackingPointerNotation begin = &fib[0];

    SomeTrackingPointerNotation end = &fib[ fib->Length ];

 

    for ( ; begin != end; ++begin )

          // …

}

Our SomeTrackingPointerNotation needs to support pointer arithmetic. That is, when we write ++begin, this does not increment the address by 1, but rather by the size of the element type. For example, since array element are of type int, each increment must add sizeof(int) to the current address value.

Neither the tracking handle (^) nor its indirect cousin, the tracking reference (%), supports pointer arithmetic. Moreover, a tracking reference does not support pointer comparison, such as

begin != end

Like a native reference, a tracking reference, once initialized, serves as an alias to the underlying object to which it refers. The comparison is not of the two tracking addresses but of the values stored at those addresses. This is not the iterator semantics we need.

In a native design, we decide whether to go with a pointer or a reference declaration based on two primary factors:

(1) if the object we wish to refer to is unavailable at the time of declaration, then we must declare a pointer and set it to null. A reference requires an initial object. So does a tracking reference.

(2) if we wish to refer to more than a single object during the lifetime of the declaration, then we must also declare a pointer. A reference cannot be reset to refer to a second or subsequent object. Neither can a tracking reference.

What this suggests is that we need an analogous choice when the constraints of a tracking reference make it ill-suited to our design. A third, more flexible form of tracking entity, the interior pointer (interior_ptr<>), is given over to this role. It can refer to no object (but only by setting it to nullptr, not 0). And it can be reset to refer to a second or subsequent object. Moreover, it supports both pointer arithmetic and pointer comparison. For example,

int sum( array<int> ^arr )

{

    if ( ! arr )

         return 0;

 

    interior_ptr<int> begin = nullptr;

    interior_ptr<int> end = &arr[ fib->Length ];

    int sum = arr[0];

 

    for ( begin = &arr[1]; begin != end; ++begin )

          sum += *begin;

 

    return sum;

}

The declaration of an interior pointer is limited to local objects, including function parameters and return types. If we don't provide an initial value, the compiler automatically inserts code to set it to nullptr – so the explicit initialization in the above example is not strictly necessary. The type specified within the template brackets identifies the kind of object addressed; we do not indicate a pointer within the brackets unless we intend two or more levels of indirection. For example,

public ref class Matrix sealed {

    float *m_mat;

 

public:

    property interior_ptr<float*> Mat

    {

        interior_ptr<float*> get(){ return &mp_mat; }

    }

 

    // …

};