In the previous post I have discussed the basic usage of C++26’s latest (still experimental) feature called reflection. In this post we will look into a more concrete problem which many of us have come across called JSON serialization. We often receive the data in the form of an object of a user defined class. And what we need to do is to format it in JSON format which means to associate the data member names in the class definition with their respective values and put them in a JSON structure. First lets have a look at a hypothetical CanDataSource class as below:
class CanDataSource
{
public:
CanDataSource(std::string id,
std::string canLine,
int bitrate,
bool extendedFrame,
std::vector<unsigned int> pgnPassFilter,
RetryPolicy retryPolicy)
: m_id{std::move(id)},
m_canLine{std::move(canLine)},
m_bitrate{bitrate},
m_extendedFrame{extendedFrame},
m_pgnPassFilter{std::move(pgnPassFilter)},
m_retryPolicy{retryPolicy}
{
}
private:
friend struct ManualJson;
std::string m_id;
std::string m_canLine;
int m_bitrate;
bool m_extendedFrame;
std::vector<unsigned int> m_pgnPassFilter;
RetryPolicy m_retryPolicy;
};This represents the CAN data that is being received from some source and is subject to further processing into JSON format by the receiver. Along with this we also have another structure representing the retry policies that are applied for transmitting the CAN data packets as the following:
struct RetryPolicy
{
int maxAttempts;
int backoffMs;
bool jitterEnabled;
};Notice that the RetryPolicy is part of the CanDataSource class hence is formatted into JSON as a part of the CanDataSource object when received at the receiver. Now, to serialize this CanDataSource object into a JSON format we can write a manual serializer implementation like the below:
static Poco::JSON::Object toJson(const CanDataSource& source)
{
Poco::JSON::Object jsonObject;
jsonObject.set("id", source.m_id);
jsonObject.set("canLine", source.m_canLine);
jsonObject.set("bitrate", source.m_bitrate);
jsonObject.set("extendedFrame", source.m_extendedFrame);
jsonObject.set("pgnPassFilter",
toHexArray(source.m_pgnPassFilter));
jsonObject.set("retryPolicy", toJson(source.m_retryPolicy));
return jsonObject;
}A very similar implementation for the policy data would be as follows:
static Poco::JSON::Object toJson(const RetryPolicy& policy)
{
Poco::JSON::Object jsonObject;
jsonObject.set("maxAttempts", policy.maxAttempts);
jsonObject.set("backoffMs", policy.backoffMs);
jsonObject.set("jitterEnabled", policy.jitterEnabled);
return jsonObject;
}These implementations are supported by Poco library which provides in-built functions to handle JSON data. Notice this implementation is hard coding each member of the CanDataSource class into its corresponding string name that needs to be attached to represent it in the JSON format.
The video below provides a quick visual introduction to JSON serialisation:
I have briefly discussed the down side of this kind of manual implementation in my last post. One of the obvious issue is that is any change in the class definition (addition or deletion of any data member) has to be followed by changes in the code. Now with reflection in C++26, the beauty is that you can reflect the non static data members of the class using the std::meta::info extracted by ^^T expression and the std::meta::nonstatic_data_members_of function in the following way:
std::meta::nonstatic_data_members_of(
^^T,
std::meta::access_context::unchecked())This will return a std::vector<std::meta::info>. To iterate over it using template for you need to convert it into a range backed by static storage. One way to do that is:
static constexpr auto members = std::define_static_array(
std::meta::nonstatic_data_members_of(
^^T,
std::meta::access_context::unchecked()));Now the members can be iterated over using template for like the following:
template for (constexpr auto member : members)
{
std::cout << std::meta::identifier_of(member) << "\n";
}You can read more about template for here. The std::meta::identfier_of function returns a std::string_view and can be printed as follows on the console:
m_id
m_canLine
m_bitrate
m_extendedFrame
m_pgnPassFilter
m_retryPolicy
maxAttempts
backoffMs
jitterEnabledThere may be some additional cleaning you may have to do. For example, if you class members are prefixed with m_ then you might like a helper function to remove that somewhat like the following:
consteval std::string_view strip_member_prefix(std::string_view name)
{
return name.starts_with("m_") ? name.substr(2) : name;
}And finally, you may like a function template that will take a reference to a JSON object, a key, and its corresponding value and will add it to the JSON object. A possible implementation will look like the following:
template <typename T>
void add_field(Poco::JSON::Object& jsonObject,
std::string_view key,
const T& value)
{
if constexpr (std::is_same_v<T, std::vector<unsigned int>>)
jsonObject.set(std::string(key), to_hex_array(value));
else if constexpr (std::is_same_v<T, std::string>)
jsonObject.set(std::string(key), value);
else if constexpr (std::is_class_v<T>)
jsonObject.set(std::string(key), to_json(value));
else
jsonObject.set(std::string(key), value);
}Now if your CanDataSource changes, there is no need to modify the code; it will be automatically reflected in the JSON output.
Full Listing
#include "can_data_source.h"
#include <Poco/JSON/Array.h>
#include <Poco/JSON/Object.h>
#include <iostream>
#include <meta>
#include <sstream>
#include <string_view>
#include <type_traits>
consteval std::string_view strip_member_prefix(std::string_view name)
{
return name.starts_with("m_") ? name.substr(2) : name;
}
template <typename T>
Poco::JSON::Object to_json(const T& source);
Poco::JSON::Array to_hex_array(const std::vector<unsigned int>& values)
{
Poco::JSON::Array jsonArray;
for (const unsigned int value : values)
{
std::ostringstream stream;
stream << std::uppercase << std::hex << value;
jsonArray.add(stream.str());
}
return jsonArray;
}
template <typename T>
void add_field(Poco::JSON::Object& jsonObject,
std::string_view key,
const T& value)
{
if constexpr (std::is_same_v<T, std::vector<unsigned int>>)
jsonObject.set(std::string(key), to_hex_array(value));
else if constexpr (std::is_same_v<T, std::string>)
jsonObject.set(std::string(key), value);
else if constexpr (std::is_class_v<T>)
jsonObject.set(std::string(key), to_json(value));
else
jsonObject.set(std::string(key), value);
}
template <typename T>
Poco::JSON::Object to_json(const T& source)
{
Poco::JSON::Object jsonObject;
static constexpr auto members = std::define_static_array(
std::meta::nonstatic_data_members_of(^^T,
std::meta::access_context::unchecked()));
template for (constexpr auto member : members)
{
std::cout << std::meta::identifier_of(member) << "\n";
constexpr std::string_view key =
strip_member_prefix(std::meta::identifier_of(member));
add_field(jsonObject, key, source.[:member:]);
}
return jsonObject;
}
int main()
{
const CanDataSource source{"can-source-1",
"can1",
250000,
true,
{0xFEF1, 0xFEEE},
RetryPolicy{3, 500, true}};
to_json(source).stringify(std::cout, 4);
std::cout << "\n";
return 0;
}Compile and Run
g++ -std=c++26 -freflection \
-Wl,-rpath,/opt/gcc-16.1/lib64 \
reflective_serialisation.cpp \
-o can_config_reflection \
-lPocoJSON -lPocoFoundation
./can_config_reflection
{
"bitrate": 250000,
"canLine": "can1",
"extendedFrame": true,
"id": "can-source-1",
"pgnPassFilter": [
"FEF1",
"FEEE"
],
"retryPolicy": {
"backoffMs": 500,
"jitterEnabled": true,
"maxAttempts": 3
}
}I have curtailed the output to fit the code window. The full sourc code is available from github link at the bottom of this post. Hope this helps. I will come up with more examples shortly. For details on how to setup the C++26 compilation environment, please refer to this post.
The complete source code used in this article is available on GitHub.
View Source Code on GitHubC++26 Series
Follow my C++26 series as I explore the language’s new features
through practical examples.
- 01 Compile Your First C++26 Program with GCC 16.1
-
02
C++26: What Is
template for? - 03 C++26: What Is Reflection and How Do You Use It?
- 04 C++26 Reflection: Simplifying JSON Serialization
- 05 C++26 Reflection Annotations: Automated Member Validation
- 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.
The scrolling sidebar things make this unreadable, unfortunately
Yes that’s an an issue, I am fixing it now. Thanks for pointing out Johann.