Differences between the Modern and Traditional C++ APIs

The modern C++ API feels very different from the traditional one. Understanding a few key concepts and the main differences with respect to the traditional C++ API can help you write code that is much more concise and robust.

The modern C++ API Conventions document describes the intended use of the Connext DDS Modern C++ API.

This article highlights some of the most relevant differences between the modern C++ API and the traditional C++ API.

Code organization

The code organization in the headers and namespaces is different between the two C++ APIs.
In the modern C++ API, the headers and namespaces are organized such that anything belonging to the standard API lives in the 
dds namespace. RTI extensions to the standard API can be found in the  rti namespace.

You can include the entire standard API by using #include <dds/dds.hpp>.
Similarly, you can include the standard API along with all the RTI extensions by using
#include <rti/rti.hpp>.
This section of the documentation explains the internal structures of these two namespaces in more detail.

The modern C++ API provides an explicit distinction between types/functions in the standard API and those in the extended API.
The most notable of these distinctions is the way in which
extension methods belonging to a standard type are accessed.

In Connext 6.0.0 and later:
Using the 
.extensions() API (e.g., participant.extensions().add_peer())

In Connext 5.2.0 and later:
Using the overloaded 
-> operator (e.g., participant->add_peer())

Generic programming

In contrast to the traditional C++ API (which barely uses templatized code), the modern C++ API is heavily templatized.
DataReaders
, DataWriters and Topics are all templatized on the TopicType they work with.

// Creating a Topic with user-defined type Foo and topic name MyTopic1
dds::topic::Topic<Foo> topic1(participant, "MyTopic1");
// Creating a DataWriter for type Foo (Publisher created inline)
dds::pub::DataWriter<Foo> writer1(dds::pub::Publisher(participant), topic1);

TopicTypes

Valid template arguments for Topics, DataWriters and other templatized classes are TopicTypes. Top-level types generated by rtiddsgen, dds::core::xtypes::DynamicData and Builtin types are all valid TopicTypes. Attempting to use an invalid TopicType in the instantiation of a templated class will result in a compilation error.

Traits

There are a number of traits that provide additional information and utilities for IDL-generated types. One of the utilities provided is the helper function dds::topic::is_topic_type<T>, which indicates whether or not a type is a valid TopicType.

DynamicData

DynamicData samples are data samples whose types are unknown at compile time. They are valid TopicTypes (and therefore can be used as template arguments).

In the modern C++ API, the generic member function  DynamicData::value<T>() allows you to  access the members of a DynamicData object. Conversely, traditional C++ API provides a separate get/set function for each primitive or string type (e.g.,  DDSDynamicData::get_short()).

This programming How-To provides code examples.

Type system & automatic memory management

Memory management in the modern C++ API is automatic. Most types fall into one of two categories; value types and reference types. Pointers are seldom used in the modern C++ API and their use at an application level is actively discouraged. As explained in this Knowledge Base article, anything in the traditional C++ API that was modeled as a pointer is likely a reference in the modern C++ API.

Value Types

Value types provide deep-copy semantics. Each value type is uniquely created and destroyed. They can be copied, moved and compared between one another.

dds::core::Duration, dds::pub::qos::DataWriterQos and any IDL TopicTypes are all examples of value types.

Reference Types

Reference types are used to represent a shared entity or resource. Internally, reference types use a reference count to keep track of how many references to the object currently exist. This count is automatically modified by Connext DDS when different operations are used on the objects. If the reference count reaches 0, the object is automatically destroyed. Reference types provide the following functionality:

  • A constructor to create new objects. Constructing an object from dds::core::null creates an instance which doesn’t reference any object.
  • A copy constructor, which when called creates a new reference to an already existing object (increasing the internal reference count in the process).
  • An assignment operator, used to replace one reference with another (possibly modifying the internal reference counts of the objects in the process). Assigning an object to dds::core::null leaves the reference empty and reduces the internal reference count.
  • An equal operator, to check if the referenced objects of two references are the same.
  • retain(), which prevents Connext DDS from deleting the underlying object, even if the internal reference count reaches 0.
  • close(), which forces the destruction of the referenced object (and can be used to close an object that has been retained).

The following code snippet demonstrates some of the features of reference types.

using namespace dds::domain;
void create_participant()
{
    // Creating a new DomainParticipant object; the internal reference count will equal 1
    DomainParticipant participant(DOMAIN_ID);
    participant.retain();
} // participant goes out of scope, reducing the internal reference count to 0. Since we called retain(), the entity is not deleted

void test_retain()
{
    create_participant();
    DomainParticipant participant = find(DOMAIN_ID);
    // We need to explicitly close the participant since we called retain()
    participant.close();
}

dds::core::Entity (and all of its subclasses) and rti::pub::FlowController are examples of reference types.

For more information see, Why doesn’t my DomainParticipant get deleted.

Constructors instead of factories

In the modern C++ API most objects are created through constructors, unlike the traditional C++ API which models the creation of DDS entities using factories.

For example, to create a DomainParticipant object in the modern C++ API:

dds::domain::DomainParticipant participant(...);

Whereas, creating a DomainParticipant object in the traditional C++ API would look like this:

DDSDomainParticipant *participant = DDSTheParticipantFactory->create_participant(...);

Type erasure—Any* entities

For each templatized subclass of dds::core::Entity (such as, Topic<T>, DataReader<T>, DataWriter<T>) the API provides a type-erased version (AnyTopic, AnyDataReader, AnyDataWriter). You can easily convert from the typed entity to its type-erased version:

DataWriter<Foo> foo_writer = …;
AnyDataWriter any_writer = foo_writer;
DataWriter<Foo> foo_writer_again = any_writer.get<Foo>();

Note that foo_writerany_writer and  foo_writer_again reference the same DataWriter.

Type-erased entities are useful in several situations. For example, to add entities with a different TopicType  to the same container:

std::vector<AnyDataWriter> writers;
DataWriter<Foo> foo_writer = …;
DataWriter<Bar> bar_writer = …;
writers.push_back(foo_writer);
writers.push_back(bar_writer);

You can also use them to look up an Entity even if you don’t know its TopicType:

auto any_writer = rti::pub::find_writer_by_name<AnyDataWriter>(publisher, “MyWriter”);

if (any_writer.type_name() == “Foo”) {
   auto foo_writer = any_writer.get<Foo>();
}

C++11 support

The modern C++ API is designed to integrate with C++11. At compile time, the API detects which C++11 features are supported and makes use of them where available. In order to enable the features, you need to ensure that C++11 support is enabled in your compiler.

When using rtiddsgen, you can pass C++11 as the argument to the  -language flag.

Some of the supported C++11 features include:

  • Move constructors and move-assignment operators for most types (both in the API and in types generated from IDL)
  • The use of range for-loops for classes such as dds::sub::LoanedSamples
  • Lambda functions can be used  in place of certain callback functions (e.g. in dds::sub::cond::ReadCondition)

The following code snippet shows the use of a range based for-loop and a lambda function together:

int count = 0;

dds::sub::ReadCondition read_condition(
    reader, 
    dds::sub::status::DataState::any(),
    [&reader, &count]()
{
    // This functor will be called when the read_condition is true
    // Take all samples
    auto samples = reader.take();
    for (const auto& sample : samples) {
        if (sample.info().valid()) {
            count++;
            std::cout << sample.data() << std::endl;
        }
    }
} // The LoanedSamples destructor returns the loan automatically
);

The full list of C++11 features which the modern C++ API has been designed to work with are described here.

Exceptions instead of return codes

While the traditional C++ API uses return codes to model errors, the modern C++ API models errors using exceptions. Each return code has a corresponding exception in the dds::core namespace. For example:

DDS_RETCODE_NOT_ENABLED -> dds::core::NotEnabledError

Return codes that do not indicate an exceptional situation are represented in a different way.

  • DDS_RETCODE_OK -> absence of exception
  • DDS_RETCODE_NO_DATA -> empty collection, or false boolean, depending on the operation

You can use the dds::core::Exception::what() API to obtain a description of the error that was thrown:

try {
    connextdds_application();
} catch (const std::exception& ex) {
    std::cerr << “Exception in connextdds_application(): “ << ex.what << std::endl;
}

Other API differences

Reading data: LoanedSamples

The LoanedSamples class provides temporary access to a collection of samples from a DataReader. The destructor of LoanedSamples automatically returns the samples, reducing the complexity of your code.

dds::sub::LoanedSamples<Foo> samples = reader.take();

for (const auto& sample : samples) {
    if (sample.info().valid()) {
        std::cout << “Received: “ << sample.data() << std::endl;
    }
} // No need to explicitly return the loan, the destructor of LoanedSamples takes care of it

This can be further simplified by using valid_data(). This function returns a view of the LoanedSamples collection whose iterators skip invalid-data samples:

auto samples = rti::sub::valid_data(reader.take());

for (const auto& sample : samples) {
    std::cout << “Received: “ << sample.data() << std::endl;
}

Sometimes a LoanedSamples container can be limiting. The API provides additional ways to access the data:

Reading data: Selector

The Selector class is used by a DataReader to compose read and take operations.

A Selector can be used to perform complicated read (or take) operations; they are created through the DataReader.select() API. Selectors allow you to configure:

  • The maximum number of samples to read/take (default: set by QoS)
  • The sample, view and instance states to read/take (default: all)
  • A QueryCondition to apply to the samples in the read/take (default: all)
  • Which instance to read/take (default: all)
  • Whether to call read() or take()
dds::sub::LoanedSamples<Foo> samples = reader.select() // Create a Selector
    .max_samples(5)                                    // Take at most 5 samples
    .state(dds::sub::status::DataState::new_data())    // Take only new data from alive instances
    .content(dds::sub::Query(reader, “x > 10”))        // Take only samples where x > 10
    .instance(instance_handle)                         // Only take instances with this handle
    .take();                                           // Call take (as opposed to read)

Creating listeners: NoOp*Listener

Certain Listener classes have a corresponding NoOpListener class which inherit from them (e.g., NoOpDataReaderListener, NoOpTopicListener). These are convenience classes that simply override each method of the base Listener to do nothing.

One scenario in which this is useful is if you only want to implement a subset of the callbacks within a Listener class (e.g., you only require the  on_data_available callback from the DataReaderListener class).

If your listener inherits directly from DataReaderListener, you will have to override each method provided by the base class, since they are pure virtual. Using the  NoOp*Listener classes avoids this since all of the methods are implemented (to do nothing).

QoS management

In the modern C++ API, access to XML QoS definitions is managed through the dds::core::QosProvider class. The default QoS values are encapsulated in the default QosProvider (obtained using QosProvider::Default()).

To create an entity with a Qos profile, use the QosProvider; the modern C++ API doesn't provide the "create with profile"-style functions, such as DDSDomainParticipantFactory::create_participant_with_profile(). The following code achieves the same result, loading a profile from the default locations, such as USER_QOS_PROFILES.xml in the current directory:

const auto& participant_qos = dds::core::QosProvider::Default().participant_qos("MyLibrary::MyProfile");
dds::domain::DomainParticipant participant(0, participant_qos);

Or you can load a specific file:

dds::core::QosProvider my_provider("my_qos.xml");
dds::domain::DomainParticipant participant(0, my_provider.participant_qos("MyLibrary2::MyProfile2"));

There are multiple ways to set QoS policies within a QoS object:

  • Through the “<<” operator:

dds::sub::qos::DataReaderQos reader_qos;
dds::core::policy::Reliability reliability;
reader_qos << reliability.kind(ReliabilityKind::RELIABLE);

  • Using the policies constructor:

reader_qos << Reliability::Reliable();
  • Using the QoS object’s policy() setter. These setters are templatized on the QoS policy:

reader_qos.policy<Reliability>(Reliability::Reliable());

  • Using the QoS object’s policy() getter to set a specific field within a policy. As with the setters, these getters are templatized on the QoS policy. They return a reference to the QoS policy object, which can then be modified:

reader_qos.policy<Reliability>().kind(ReliabilityKind::RELIABLE);

Looking up entities

In the traditional C++ API, the lookup_entity APIs exist to allow you to obtain a reference to an object. In the modern C++ API, these APIs have been replaced by  find_entity APIs. These  find_entity APIs provide standalone namespace-level functions to find DDS entities, as opposed to member functions in the factory entities, as in the traditional C++ API.

Using these functions it is possible to choose whether the returned entity is typed or type-erased (note that in the example both foo_writer and any_writer contain a reference to the same entity):

auto foo_writer = rti::pub::find_datawriter_by_name<DataWriter<Foo>>(publisher, “MyDataWriter”);
AnyDataWriter any_writer = rti::pub::find_datawriter_by_name<AnyDataWriter>(publisher, “MyDataWriter”);

IDL binding

Structures and unions defined in IDL files are translated to classes in C++ by using rtiddsgen. There are several differences in this mapping with respect to the traditional C++ API:

  • These types have value-type semantics. They include a default constructor, copy constructor, move constructor, copy-assignment operator, and move-assignment operator.
  • Instead of providing direct access to the fields, setters and getters are generated.
  • IDL string and sequence members map to STL or STL-friendly types, namely std::string, std::vector, and rti::core::bounded_sequence.
  • Optional members map to dds::core::optional (similar to std::optional) instead of pointers.

For a full description of the mapping between IDL and C++ classes, please see this Programming How-To.

Programming Language:

Comments

Moving from rtiddsgen 3.1.2 to 4.2.0 for the C++11 target, I see that the generated classes no longer implement the move semantics (move constructor and move-assignement operator).  Is this intentional?  I can't find any release documentation to indicate whether this is the case.

The explicit definitions of the move constructor and move-assignment operator are inside #ifdef RTI_CXX_NO_IMPLICIT_MOVE_OPERATIONS , which is only defined for compilers with early/limited C++11 support, where the implicit move and assignment operators are not defined by the compiler. We no longer support any of those platforms on our current release, so the workaround is no longer necessary.
The types we generate support move semantics, because the compiler-generated move operations suffice.