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:

Now this class has private members m_rpm, m_maxTemperature and m_deviceName that are initialised to certain values at the time of object creation construction via the constructor. Now, lets say you don’t want the class instance to be created with values that lies outside a certain range and you want to make sure that the validation occurs at the time of construction. What are the options you have? You can do something like the below in the constructor:

Great! but what if another class member is added to the class whose values also needs validation, you have to add the validation code manually in the constructor to take care of the new member. Manual addition is error prone. In C++26, we can utilise feature like annotation and use it with feature like reflection to automate this validation process.

Use Annotation

C++26 comes up with the concept of annotation. The general syntax of annotation is:

Which means you can associate constant expression along with your declaration and later at compile time you can retrieve those values. We will look at that shortly. Example of such annotation will be like the below:

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

User-Defined Type for the Annotation

You can also use a used defined type as the annotation for your declaration like the following:

For example in our example code we can declare annotations of type Range and Required as the following:

Once that is declared we can now rewrite the MotorConfig class as the below using user defined type annotation:

Look closer into the decalration:

It indicates that the permitted range for the member variable m_rpm is 0.0 to 10000.0. At this point, however, the annotation is only additional information. It does not enforce the range by itself and can be ignored unless code later on forces it.

Now reflect it

Once your annotations are in place as shown in the above code, we can now 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 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 extract the annotation(s) of each of the members:

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. 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

Remember we have only one annotation in one type:

But it is also possible to have multiple annotation in one type:

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

Or separately as the following:

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

In our case we have only one annotation and reflection of which can be denoted by ranges[0]. To extract the usable range we can do the following:

The Splice Expression

We are getting to a point where we can access the current value of each member and compare it with the 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 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 is the code snippet which does all of these:

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

Validate at Construction

The validation is implemented in the following template function:

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!


Discover more from Tech For Talk

Subscribe to get the latest posts sent to your email.

Leave a Reply