Part of C++26 Series Explore the series

In my exploration of the C++26 series, I have so far covered how to set up your C++26 build environment on Ubuntu in this previous post. Then I explored another feature called template for, covered a simple use of reflection with a basic example, and progressed to JSON serialisation using reflection. In this post, I am going to explain another extremely powerful C++26 feature called annotations and how they can make our lives easier when combined with reflection.

To start with let’s take a look at the following hypothetical MotorConfig class:

This class has three private members, m_rpm, m_maxTemperature and m_deviceName, which are initialized from the values passed to the constructor when an object is created. Suppose you do not want an instance of the class to be created if one of its numeric values lies outside the permitted range, and you want the validation to occur during construction. What options do you have? You could add checks like the following to the constructor:

This works, but what happens if another member that also requires validation is added to the class? You must manually add another validation check to the constructor. This is easy to forget and can leave the new member unvalidated, and hence this approach is error prone. In C++26, annotations can be combined with reflection to automate this validation process.

Use Annotation

C++26 introduces the concept of an annotation. The general syntax of annotation is:

This associates the result of the constant expression with the declaration. Reflection code can later retrieve a reflection of that result at compile time and, when required, extract the stored value. We will look at that shortly. An example of an annotation is:

The = immediately inside [[...]] identifies this as an annotation.

We can also define more customised annotation using user defined data types.

User-Defined Type for the Annotation

You can also use a user-defined type as an annotation value for a declaration, as follows:

For our example, we can define two types, Range and Required, as follows and use them in annotations later:

Once these types are defined, we can rewrite the MotorConfig class using them to create annotation values:

Let’s take a closer look at the annotations attached to the data members:

The annotation on m_rpm creates a Range value with a minimum of 0.0 and a maximum of 10000.0. Similarly, the annotation on m_maxTemperature specifies a permitted range from -40.0 to 150.0. The Required{} annotation marks m_deviceName for required-value validation.

At this point, however, it is only information associated with the member declaration. It does not enforce the range by itself. Code must later retrieve the annotation through reflection and enforce the corresponding validation rule.

Now reflect it

Once our annotations are in place, as shown in the code above, we can leverage reflection to extract and use them in our code.

First, we will write a loop that will go over all the non-static members of the MotorConfig class:

I have discussed template for in detail in this article. Briefly, ^^T represents the reflection of the type T, which is MotorConfig in our case. std::meta::nonstatic_data_members_of returns a vector of reflections of all the non-static data members. But as the vector uses dynamic storage, it cannot be used as the range of template for. To use it with template for, we need to copy it into a static array, which we achieve using std::define_static_array.

Then, in each iteration, we retrieve the annotations attached to the current member:

The function std::meta::annotations_of_with_type returns a vector of reflections of all the annotations of type Range. std::define_static_array copies those reflections into an array with static storage and returns a std::span referring to that array. A vector normally stores its elements in dynamically allocated memory, so its contents cannot be stored directly in the constexpr variable ranges.

Single vs Multiple Annotation

Note that, in this example, each member has at most one annotation of a given type:

But it is possible for a member to have multiple annotations of the same type:

And you can express it in any of the following ways:

Or separately as the following:

So, in the following expression, ranges is a span referring to the static array of reflections of the annotations:

In our case we have only one Range annotation, and its reflection is available at ranges[0]. To extract the corresponding Range value, we can do the following:

The Splice Expression

We can now access the current value of each member that has a Range annotation and compare it with its permitted range. The member value can be accessed using the following splice expression:

[:member:] is a C++26 splice expression. It converts the reflection back into the entity represented by that reflection. So in the first iteration object.[:member:] will give us the value of object.m_rpm and now we can do a comparison like the following:

In the next iteration the object.[:member:] will give us the value of object.m_maxTemperature and so on. The following code snippet which does it all:

The handling of the Required annotation is very similar and can be implemented somewhat like the following:

Validate at Construction

The full listing of the validation code is as follows:

The full source code is available in this Github link. To compile the code do the following in the command line:

To enable validation right at the inception we can call the validation function template validate_or_throw in the constructor of the MotorConfig class as follows:

This will ensure that the constructor of MotorConfig throws if values outside the permitted range are passed to it. All the hassle of manually checking the passed parameter values is gone!

C++26 Series

Follow my C++26 series as I explore the language’s new features through practical examples.

  1. 01 Compile Your First C++26 Program with GCC 16.1
  2. 02 C++26: What Is template for?
  3. 03 C++26: What Is Reflection and How Do You Use It?
  4. 04 C++26 Reflection: Simplifying JSON Serialization
  5. 05 C++26 Reflection Annotations: Automated Member Validation
  6. 06 C++26 Contracts: What Do They Add Beyond Manual Checks and Assertions?

Discover more from Tech For Talk

Subscribe to get the latest posts sent to your email.

Leave a Reply