Howto use OMG DDS Sequences in C++ (RTI Connext DDS)

Applicability

This HOWTO applies to the "classic" C and C++ API to the OMG Data Distribution Service (DDS) in which the data-types used to hold sequences were defined as specialized containers. This is needed for C and in the case of C++ was done to comply with IDL to language mappings that proceeded the use of the standard template library.

The new standard OMG C++ API To DDS known as the ISO/IEC C++ 2003 Language DDS PSM ((DDS-PSM-C++), still in the Beta stage, uses the C++ std::vector class to hold sequences. Threrefore the Sequnce API described in this HOWTO are no longer necessary when using the new DDS-PSM-C++ API.

Background

Several of the API calls in DDS accept or return sequences of elements. Most notably the DataReader read and take operations can return sequences of application-defined datatypes and meta-data (also known as SampleInfo). In addition several of the QoS contain attributes that are sequences of values (e.g. sequences of strings, octets, etc.)

The C language lacks a standard container class to hold sequences. Typically developers use pointers for this. However this representation lacks information on things like the length of the sequence, the number of valid or allocated members etc.

The C++ language also lacked a standard container class for sequences until template-based 'std' library was added to the C++ standard in C++98. For this reason the original OMG mapping from IDL to C++ used custom-defined sequences. This has now been revised on the recently-adopted IDL to C++11 specification.

Even with support to this new specification becoming available from the vendors some embedded platforms may still prefer to use the "classic" C++ mapping because it does not use C++ templates. This has potential advantages in terms of reduced code size and ease of safety certification.

Defining sequences in RTI Connext DDS classic C++

Sequences in RTI Connext DDS classic C++ API are mapped to classes that are generated by rtiddsgen from a Type-definition given in one of the supported type-definition languages (IDL, XML, XSD). For example. Assume the following type defined in the IDL file named foo.idl :

struct Foo {
    long x;
    long y;
};

Or equivalently in the XML file foo.xml:

<?xml version="1.0" encoding="UTF-8"?>
<types>
    <struct name="Foo">
        <member name="x" type="long"/>
        <member name="y" type="long"/>
    </struct>
</types>

If we run rtiddsgen on this file we will generate a foo.h file that contains a definition for the types Foo and FooSeq:

/* Extract of contents of foo.h */

#include "ndds/ndds_cpp.h"

struct FooSeq;

class Foo  {
public:            
    typedef struct FooSeq Seq;

    DDS_Long  x;
    DDS_Long  y;  
};       

DDS_SEQUENCE(FooSeq, Foo);

No surprises here. Note that the mapping to C++ used the type DDS_Long to ensure a portable 32-bit integer across platforms. In most platforms this will just be a typedef to 'int'.

In addition note the use of the 'DDS_SEQUENCE' to actually define the sequence.

This is actually using a macro defined in "ndds/ndds_cpp.h" the use of this macro (instead of explicit code generation) has two benefits. First it allows the actual implementation of the sequence to be changed without forcing a re-generation if the code. And second it provides the ability for a user to define their own RTI DDS compatible sequences directly in C++ without having to use the 'rtiddsgen' code generator.

As an illustration of the second point, a user can define a sequence of their own C++ defined type as follows:

/* Example my_own_sequence.h  */
 #include "ndds/ndds_cpp.h"

struct MyTypeSequence;

/* This class is not defined in IDL or XML/XSD and not generated with rtiddsgen */
class MyType  {
public:            
    SomeType1 member1;
    SomeType2 member2;  
    SomeType3 member3;  
}; 

DDS_SEQUENCE(MyTypeSequence, MyType);

With these definitions the user has now a type called MyTypeSequence with the same characteristics as described here.

Using sequences in RTI Connext DDS

Memory management: Sequence-owned or Loaned-to-sequence

A sequence is a container for the elements within it. Sequences have a 'capacity' representing the maximum number of elements they can hold and a 'length' that represents the current number of elements in the sequence. The sequence API allows modifying and retrieving these values.

Since the sequence holds a set of elements a key question arises: Who is responsible for creating and managing the memory used to store those elements? There are two possible approaches to this:

  1. Sequence-owned: The sequence object itself creates and manages the memory of the elements within. When such a sequence is deleted it will automatically free the memory used to hold the elements and call the destructor on each of the elements present.
  2. Loaned-to-sequence: The memory is managed outside and "loaned" to the container which can use it until it is "taken away" from it. When this sequence is deleted it does not release the memory for the elements or call their destructor.

RTI Connext DDS sequences support both the above memory-management models. This is done is to allow optimal performance and predictable real-time behavior.

You can tell which kind of sequence you have by calling the operation "has_ownership" on the sequence. This operation will return the bolean value 'true' if the sequence owns the memory for its elements and false otherwise.

Creating Sequences that own the memory of their elements

By default when a sequence is first created it has a zero capacity (and therefore length). Such sequences have no resources allocated for their members so they are treated as Sequence-owned because you can delete them and they will take care of "removing" all the contained resources.

You can verify by executing the code snippet below (the code example here is all included in the attached ZIP file).

    FooSeq *myOwnedSequence = new FooSeq();

    printf("myOwnedSequence.lengh()    = %d\n", myOwnedSequence->length());
    printf("myOwnedSequence.maximum()  = %d\n", myOwnedSequence->maximum());
    printf("myOwnedSequence.has_ownership()  = %d\n", myOwnedSequence->has_ownership());
    delete myOwnedSequence;

When this code is compiled and executed it produces the following output:

myOwnedSequence.lengh()    = 0
myOwnedSequence.maximum()  = 0
myOwnedSequence.has_ownership()  = 1

Confirming that the sequence is indeed empty and owns the resources for the (empty set of) elements.

The Sequence-owned model is easiest to program if the application just needs to create a sequence from scratch. You can easily modify the capacity. Set the length, the values for the elements, etc. As the sequence manages all the resources when you are done you can just delete the sequence and all resources will be freed.

The difference between the capacity and length is this. The 'capacity' indicates the memory allocated to hold the elements of the sequence. As such it indicates the maximum number of elements the sequence can hold. For this reason the operation to access its value is called 'maximum()'. A sequence however may contain fewer elements than the ones allowed by the capacity. This is what the 'length()' operation on the sequence returns.

Clearly it is always the case that for any sequence length() <= maximum(). In the case where length() < maximum(), there are fewer elements than the capacity allows:  memory for those extra elements is allocated, but not used. Those elements may not be initialized and are therefore not accessible using the sequence API.

The motivation for allowing a sequence to have a length different from the capacity is that memory allocation and free is expensive and non-deterministic in time. Therefore it would not be efficient if each time a sequence changed length the container had to allocate/free memory. For this reason the application can control precisely when the memory operations will occur which happens only when the capacity is modified (or when the sequence is deleted).

Therefore the first thing a user must do before using a new sequence is set its capacity using either the 'maximum()' or the 'ensure_length()' operations. If we tried to set the length or access a member before that we would get an error.

This is easy to see in the following code snippet:

    FooSeq myOwnedSequence2;
    printf("myOwnedSequence2.length()   = %d\n", myOwnedSequence2.length());
    printf("myOwnedSequence2.maximum()  = %d\n", myOwnedSequence2.maximum());
    printf("Note: This will generate an error because we are trying to get an element\n"
            "      at an index (one) that exceeds the length (currently zero)\n");
    Foo *myElement = &myOwnedSequence2[1];

When this code is compiled and executed it produces the following output:

myOwnedSequence2.length()   = 0
myOwnedSequence2.maximum()  = 0
Note: This will generate an error because we are trying to get an element
      at an index (one) that exceeds the length (currently zero)
FooSeq_get_reference:!assert index out of bounds

Note the use of the syntax:

    Foo *myElement = &myOwnedSequence2[1]; 

This tries to access a pointer to the second element (the one at index 1) in the sequence. We could also have used the syntax below:

    Foo myElement = myOwnedSequence2[1]; 

This is a valid way to access elements. Despite the looks this syntax does not allocate an extra copy because the '[]' operation returns a reference to the element, not the element itself.

However as this particular call returns an error in the access of the element we chose to take the address of the return because otherwise the setting of the reference would cause a segmentation fault. This is a limitation of the sequence API. Because it is designed to not throw exceptions it cannot notify of such errors in a way that can be handled by the application and consequently there is no way to recover from this error.

The code above shows that you cannot access elements in the sequence that are beyond the length. This is done with the 'length()' operation.

However the 'length()' operation is not allowed to allocate memory, so it can only set a length that it less or equal to the capacity. As the capacity is initially zero the first thing we need to do is set the capacity to a reasonable value for our purposes.

If we tried to set the length to a value that exceeds the capacity we would also get errors as shown in the code snippets below:

    printf("\nCalling: myOwnedSequence3.length(10)\n");
    printf("\nNote: This will generate an error because we are trying to set the\n"
            "     length to a value (10) that exceeds the capacity (currently zero)\n");
    myOwnedSequence3.length(10);
    printf("\nNote: Length remains unchanged\n");
    printf("myOwnedSequence3.length()   = %d\n", myOwnedSequence3.length());

    printf("\nCalling: myOwnedSequence3.maximum(5)\n");
    myOwnedSequence3.maximum(5);
    printf("myOwnedSequence3.maximum()  = %d\n", myOwnedSequence3.maximum());
    printf("\nCalling: myOwnedSequence3.length(10)\n");
    printf("Note: This will generate an error again because we are trying to set the\n"
            "     length to a value (10) that still exceeds the capacity (currently 5)\n");
    myOwnedSequence3.length(10);
    printf("\nNote: Length remains unchanged\n");
    printf("myOwnedSequence3.length()   = %d\n", myOwnedSequence3.length());

When this code is compiled and executed it produces the following output:

Calling: myOwnedSequence3.length(10)

Note: This will generate an error because we are trying to set the
     length to a value (10) that exceeds the capacity (currently zero)
FooSeq_set_length:available space 0 < 10

Note: Length remains unchanged
myOwnedSequence3.length()   = 0

Calling: myOwnedSequence3.maximum(5)
myOwnedSequence3.maximum()  = 5

Calling: myOwnedSequence3.length(10)
Note: This will generate an error again because we are trying to set the
     length to a value (10) that still exceeds the capacity (currently 5)
FooSeq_set_length:available space 5 < 10

Note: Length remains unchanged
myOwnedSequence3.length()   = 0

So the correct way to do is to set first the maximum and then the length to a value less or equal to the maximum as shown here:

    printf("\nCalling: myOwnedSequence3.maximum(10)\n");
    myOwnedSequence3.maximum(10);
    printf("myOwnedSequence3.maximum()  = %d\n", myOwnedSequence3.maximum());
    
    printf("\nCalling: myOwnedSequence3.length(10)\n");
    myOwnedSequence3.length(10);
    printf("\nNote: Length has now changed to the requested value\n");
    printf("myOwnedSequence3.length()   = %d\n", myOwnedSequence3.length());

When this code is compiled and executed it produces the following output:

    Calling: myOwnedSequence3.length(10)
    
    Note: Length has now changed to the requested value
    myOwnedSequence3.length()   = 10

A useful convenience operation: ensure_length()

Sometimes we just know the length we need and are wanting to increase the capacity of the sequence if needed. If we knew the capacity at this point we could conditionally call maximum(desired_length) in the situations were we need to increase it. Of course we can always call maximum() to get the current capacity. But doing all this involves calling multiple functions and adding if statements which is cumbersome if all we need it to make sure that the capacity is big enough.

Fot this reason there is the convenience operation ensure_length(). This operation implements all the logic described above and then sets the length. In other words it checks that the capacity is sufficient, and if not so it increases it so length of the sequence can be resized to the desired value. Note that this operation only resizes the sequence if the desired length is too big for the current capacity.

The code below illustrates this:

    FooSeq myOwnedSequence4;
    printf("\n\nmyOwnedSequence4.length()   = %d\n", myOwnedSequence4.length());
    printf(    "myOwnedSequence4.maximum()  = %d\n", myOwnedSequence4.maximum());

    printf("\nCalling: myOwnedSequence4.ensure_length(10, 20)\n");
    myOwnedSequence4.ensure_length(10, 20);

    printf("\nNote: Both the capacity and length are set to the desired value\n");
    printf(    "myOwnedSequence4.length()   = %d\n", myOwnedSequence4.length());
    printf(    "myOwnedSequence4.maximum()  = %d\n", myOwnedSequence4.maximum());

When this code is compiled and executed it produces the following output:

myOwnedSequence4.length()   = 0
myOwnedSequence4.maximum()  = 0

Calling: myOwnedSequence4.ensure_length(10, 20)
Note: Both the capacity and length are set to the desired value
myOwnedSequence4.length()   = 10
myOwnedSequence4.maximum()  = 20

Notice how the capacity is actually set to 20, not 10. The ensure_length() operation takes 2 parameters. The first is the new desired length. The second is the new capacity in case the capacity needs to be increased. Clearly the value passed for the new capacity should be greater or equal to the new desired length. Doing otherwise would be non-sensical and cause an error to be returned. In many cases you would just specify the same value for both parameters. However as memory allocation is expensive it makes sense in other situations to pass a bigger value for the new capacity in case that later in the code we try to use slightly bigger lengths. Rather than increasing capacity with any small length increase the second paramater allows the application to cause the sequence to allocate memory when the needed length increases significantly.

We can see this at play in the following code. As the capacity was set to 20, the new call to ensure_length(15, 30) changes only the length but not the capacity.

    printf("\nCalling: myOwnedSequence4.ensure_length(15, 30)\n");
    myOwnedSequence4.ensure_length(15, 30);

    printf("\nNote: Since the capacity was sufficient for the new length of 15\n"
             "      the length is increased but the capacity remains the same\n");
    printf(    "myOwnedSequence4.length()   = %d\n", myOwnedSequence4.length());
    printf(    "myOwnedSequence4.maximum()  = %d\n", myOwnedSequence4.maximum());

When this code is compiled and executed it produces the following output:

Calling: myOwnedSequence4.ensure_length(15, 30)

Note: Since the capacity was sufficient for the new length of 15
      the length is increased but the capacity remains the same
myOwnedSequence4.length()   = 15
myOwnedSequence4.maximum()  = 20

Creating Sequences which borrow (use loaned) memory for their elements

Sequences that borrow memory for their elements are a bit more complicated to use. However in some situation they offer the big advantage of saving memory copies which can yield significant performance benefits and save resources.

Imagine for example you already had an array of elements that your application had created and filled with values. Now you need to put them into a sequence in order to manage them as a collection or pass them to a function that takes a sequence parameter. If all you had were sequences that owned their own memory all you could do was allocate memory for the elements in the sequence and copy each element... With sequences that can use loaned memory for the elements you can avoid all this.

The creation of the sequence is the same as before. What is different is that the application never calls the 'capacity' setting operations -- maximum(int), ensure_length(int, int), or from_array(). Rather the application initialized the elements of the sequence calling one of the 'element loaning' operations: loan_contiguous() or loan_discontiguous().

Another difference is that the application must call the operation unloan() prior to the calling the destructor on the sequence.

This code illustrates the workflow:

    printf("\nUsing loaned sequences\n");
    int FOO_ARRAY_LENGTH = 40;
    Foo *foo_array = new Foo[FOO_ARRAY_LENGTH];

    FooSeq myLoanedSequence1;
    printf("myLoanedSequence1.length()  = %d\n", myLoanedSequence1.length());
    printf("myLoanedSequence1.maximum() = %d\n", myLoanedSequence1.maximum());
    printf("myLoanedSequence1.has_ownership() = %d\n", myLoanedSequence1.has_ownership());

    printf("\nCalling: myLoanedSequence1.loan_contiguous(foo_array, 40, 40)\n");
    myLoanedSequence1.loan_contiguous(foo_array, FOO_ARRAY_LENGTH, FOO_ARRAY_LENGTH);

    printf("\nNote: myLoanedSequence1 has been resized and has_ownership() now equals 0\n");
    printf("myLoanedSequence1.length()  = %d\n", myLoanedSequence1.length());
    printf("myLoanedSequence1.maximum() = %d\n", myLoanedSequence1.maximum());
    printf("myLoanedSequence1.has_ownership() = %d\n", myLoanedSequence1.has_ownership());

    printf("\nCalling myLoanedSequence1.unloan()\n");
    myLoanedSequence1.unloan();
    printf("myLoanedSequence1.length()  = %d\n", myLoanedSequence1.length());
    printf("myLoanedSequence1.maximum() = %d\n", myLoanedSequence1.maximum());
    printf("myLoanedSequence1.has_ownership() = %d\n", myLoanedSequence1.has_ownership());

When this code is compiled and executed it produces the following output:

Using loaned sequences
myLoanedSequence1.length()  = 0
myLoanedSequence1.maximum() = 0
myLoanedSequence1.has_ownership() = 1

Calling: myLoanedSequence1.loan_contiguous(foo_array, 40, 40)

Note: myLoanedSequence1 has been resized and has_ownership() now equals 0
myLoanedSequence1.length()  = 40
myLoanedSequence1.maximum() = 40
myLoanedSequence1.has_ownership() = 0

Calling myLoanedSequence1.unloan()
myLoanedSequence1.length()  = 0
myLoanedSequence1.maximum() = 0
myLoanedSequence1.has_ownership() = 1

Purpose of the 'unloan' operation

When a sequence that borrows the memory for the elements is destroyed it does not delete the memory for the elements themselves nor calls their destructor. It cannot as the memory was allocated elsewhere and the sequence does not 'own' it. However as this would be an easy way to leak resources in a silent manner the destructor of a sequence does a check. If the destructor of a sequence is called, with the sequence having been loaned resources, it will generate a warning. To avoid this the resources should be 'reclaimed from the sequence' before it is destroyed. This is precisely what the 'unloan()' operation does.

There are other reasons to call the 'unloan()' operation. Assume that you would like to reuse that sequence for something else. Perhaps convert it into one that owns its resources. Or make it loaned but point to a different set of elements. In order to do this you first need to 'unbind' the sequence from the emory it currently has loaned to it. This again is what the 'unloan()' operation does.

After 'unloan()' is called the sequence is back to its original state of zero capacity and length.

Using Sequences with loaned memory for their elements to save allocations and copies

The following code shows the difference in the setting of elements for Sequences that own their memory and those that loan it. In both situations we assume an existing array 'foo_array' of the element type Foo.

    printf("\n\nCopying elements one-by-one into a sequence that owns its memory\n");
    // Example using owned sequences
    FooSeq myOwnedSequence5;

    // This allocates memory
    myOwnedSequence5.ensure_length(FOO_ARRAY_LENGTH, FOO_ARRAY_LENGTH);
    for (int i=0; i< FOO_ARRAY_LENGTH; ++i) {
        myOwnedSequence5[i] = foo_array[i]; // this copies each element
    }
    printf("myOwnedSequence5[10] =  { x = %d, y = %d } \n",  myOwnedSequence5[10].x, myOwnedSequence5[10].y);

    printf("\n\nUsing from_array() to copy into a sequence that owns its memory\n");
    // Alternatively we could use the convenience function from_array
    FooSeq myOwnedSequence6;

    // Note that from_array() calls internally ensure_length() so it is unnecessary to
    // set the capacity or length explicitly ahead of calling from_array()
    myOwnedSequence6.from_array(foo_array, FOO_ARRAY_LENGTH); // Copies each element

When this code is compiled and executed it produces the following output:

Copying elements one-by-one into a sequence that owns its memory
myOwnedSequence5[10] =  { x = 10, y = -10 } 

Using from_array() to copy into a sequence that owns its memory

myLoanedSequence6[10] =  { x = 10, y = -10 } 

Now we do the same with a sequence with loaned memory:

   printf("\n\nUsing sequence that owns its memory to set elements without\n"
               "allocating memory or doing extra copies.\n");
    FooSeq myLoanedSequence6;

    // This does not allocate memory. It makes the sequence point to the existing  foo_array
    // This also does not do any copies. Yet the array will end up with the same values
    myLoanedSequence6.loan_contiguous(foo_array, FOO_ARRAY_LENGTH, FOO_ARRAY_LENGTH);
    printf("myLoanedSequence6[10] =  { x = %d, y = %d } \n",  myLoanedSequence6[10].x, myLoanedSequence6[10].y);
    myLoanedSequence6.unloan();

When this code is compiled and executed it produces the following output:

Using from_array() to copy into a sequence that owns its memory
myLoanedSequence6[10] =  { x = 10, y = -10 } 

Advanced: Using Sequences that use loaned elements that are not all in a contiguous array

Sometimes we want the benefits of loaned sequences but we do not have the data in a contiguous array of elements. For example our 'Foo' objects are sitting in different places of memory, perhaps allocated one by one using 'new'. We also may want to reorder the elements in the sequence without having to copy them each time we move them.

The solution to both the above scenarios is to have a sequence that contains internally an array of pointers to the Foo objects. Then each element in this array could point to a separate Foo object independent of where it is in memory. This is what is referred as a 'Loaned sequence with a discontiguous element buffer'. Despite the name they are actually easy to use.

The following code snippet illustrates how to set one up:

    printf("\n\nUsing a loaned sequence with a discontiguous element buffer.\n");
    int FOO_PTR_ARRAY_LENGTH = 40;
    Foo **foo_ptr_array = (Foo **)malloc(FOO_PTR_ARRAY_LENGTH);

    // Set the elements in foo_array_ptr to point to Foo objects wherever they are in memory
    for (int i=0; i< FOO_ARRAY_LENGTH; ++i) {
        foo_ptr_array[i] = new Foo();
        foo_ptr_array[i]->x = i;
        foo_ptr_array[i]->y = -i;
    }

    FooSeq myLoanedDiscontiguousSequence6;
     // This does not allocate memory. It makes the sequence point to the existing  foo_array_ptr
     // This also does not do any copies. Yet the array will end up with the same values
    myLoanedDiscontiguousSequence6.loan_discontiguous(foo_ptr_array, FOO_ARRAY_LENGTH, FOO_ARRAY_LENGTH);
    printf("myLoanedDiscontiguousSequence6[10] =  { x = %d, y = %d } \n",
             myLoanedDiscontiguousSequence6[10].x, myLoanedDiscontiguousSequence6[10].y);

    // Note it is  using the same foo_array_ptr we set in the first place.
    // So no memory allocation occurred on the loan_discontiguous() call
    printf("\nReturn from get_discontiguous_buffer() = %p,  original foo_array_ptr = %p\n",
            myLoanedDiscontiguousSequence6.get_discontiguous_buffer(), foo_ptr_array);
    myLoanedDiscontiguousSequence6.unloan();

Note how we first constructed an array of pointers to Foo. Then we set the elements to point where we wanted and used that to initialise the sequence calling loan_discontiguous().

After that we can use the sequence as before. The syntax to set and get elements is the same as before and uses the '[]' operator as in myLoanedDiscontiguousSequence6[10]. The sequence handles internally any dereferences necessary to access that element.

When this code is compiled and executed it produces the following output:

Using a loaned sequence with a discontiguous element buffer.
myLoanedDiscontiguousSequence6[10] =  { x = 10, y = -10 } 

Return from get_discontiguous_buffer() = 0x100c000c0,  original foo_array_ptr = 0x100c000c0

Finally if we want to move elements around we need to acces the underlying buffer with the get_discontiguous_buffer(). This is illustrated below:

    printf("\nSwapping the elements at index 0 and 5\n");
    FooSeq myLoanedDiscontiguousSequence7;
    myLoanedDiscontiguousSequence7.loan_discontiguous(foo_ptr_array, FOO_ARRAY_LENGTH, FOO_ARRAY_LENGTH);

    printf("Value before: myLoanedDiscontiguousSequence7[0] = { x = %d, y = %d } \n",
            myLoanedDiscontiguousSequence7[0].x, myLoanedDiscontiguousSequence7[0].y);
    printf("Value before: myLoanedDiscontiguousSequence7[5] = { x = %d, y = %d } \n",
            myLoanedDiscontiguousSequence7[5].x, myLoanedDiscontiguousSequence7[5].y);

    Foo **sequence_foo_ptr_array = myLoanedDiscontiguousSequence7.get_discontiguous_buffer();
    Foo *tmp = sequence_foo_ptr_array[0];
    sequence_foo_ptr_array[0] = sequence_foo_ptr_array[5];
    sequence_foo_ptr_array[5] = tmp;

    // No need to call loan_discontiguous() or do anything else as we are directly accessing the array the
    // myLoanedDiscontiguousSequence7 uses internally.
    printf("Value after:  myLoanedDiscontiguousSequence7[0] = { x = %d, y = %d } \n",
            myLoanedDiscontiguousSequence7[0].x, myLoanedDiscontiguousSequence7[0].y);
    printf("Value after:  myLoanedDiscontiguousSequence7[5] = { x = %d, y = %d } \n",
            myLoanedDiscontiguousSequence7[5].x, myLoanedDiscontiguousSequence7[5].y);

    myLoanedDiscontiguousSequence7.unloan();

When this code is compiled and executed it produces the following output:

Value before: myLoanedDiscontiguousSequence7[0] = { x = 0, y = 0 } 
Value before: myLoanedDiscontiguousSequence7[5] = { x = 5, y = -5 } 
Value after:  myLoanedDiscontiguousSequence7[0] = { x = 5, y = -5 } 
Value after:  myLoanedDiscontiguousSequence7[5] = { x = 0, y = 0 } 

Advanced: Accessing the raw buffers with get_continuous_buffer() and get_discontinuous_buffer()

If a sequence has non-zero capacity then it has allocated (or been loaned) storage to hold the specified capacity. The format of the storage can be either a contiguous array of Foo elements as in:

  
+-------------------+
| FooSeq            |
| ...               |          +-----+-----+-----+-----+
| contiguous_buffer |  ----->  | Foo | Foo | Foo | Foo |
| ...               |          +-----+-----+-----+-----+
|                   |
+-------------------+

Or as an array of pointers which themselves point to Foo data elements that can be anywhere in memory. This is was was called sequence with a discontiguous element buffer:

+----------------------+
| FooSeq               |
| ...                  |    
| ...                  |       +------+------+------+------+
| discontiguous_buffer | --->  | Foo* | Foo* | Foo* | Foo* |
|                      |       +------+------+------+------+
+----------------------+          |      |      |      |       
                                  |      |      |      | 
                                  \/     |      |      |       
                               +-----+   |      |      | 
                               | Foo |   |      |      |
                               +-----+   \/     |      |  
                                      +-----+   |      |
                                      | Foo |   |      |
                                      +-----+   \/     |
                                              +-----+  | 
                                              | Foo |  |
                                              +-----+  \/
                                                     +-----+     
                                                     | Foo |    
                                                     +-----+       

The operations get_continuous_buffer() and get_discontinuous_buffer() can be used to get access to these raw pointers. Note that for a sequence with non-zero capacity only one of the two pointers is non-NULL. So checking which pointer is NULL can be used to determine if the sequence uses a continuous array of elements, or uses an extra array of pointers that can point to elements placed anywhere in memory.

Note that these operations provides almost no encapsulation of the sequence's underlying implementation. Certain operations, such as FooSeq::maximum(long), may render the buffer invalid. Some data, beyond FooSeq::length() can be un-initialized. In light of these caveats, these operation should be used with care.

Use of sequences by the DataReader read and take operations

The DDS DataReader internally contains a cache that holds the received sample data and SampleInfo. It would be inefficient to force a copy of this data each time the application calls read or take.

For this reason the DataReader read and take operations can use 'Loaned sequences with a discontiguous element buffer'. The pattern is as follows:

The applications creates sequences for the data (FooSeq) and for the SampleInfo (SampleInfoSeq) and leaves it with zero capacity. It then calls the read/take operations on the DataReader passing this zero-capacity sequences as containers to hold the returned data samples and sample information. The DataReader detects that these samples have zero capacity and uses the loan_discontiguous() operation to loan both the sequences of pointers and the elements themselves. So no memory allocation whatsoever occurs.

Once the application is done with the data it calls the FooDataReader::return_loan() operation passing the sequences again and this causes the DataReader to call the "unloan()" operation on the sequences leaving them back in the initial state with zero capacity.

States of the Sequence

It is helpful to look at the possible states of the sequence using a state machine notation. This notation makes it easy to see the operations that are legal to call and their consequences on the sequence.

Sequence State Machine