Abstract The new edition of the C++ standard [1], expected to be released in 2026, will add reflection to C++. This will allow developers to write code that will examine the properties of classes, types, functions, and other elements and make changes to them. At the time of writing, reflection is not supported by the […]
Abstract The new edition of the C++ standard [1], expected to be released in 2026, will add reflection to C++. This will allow developers to write code that will examine the properties of classes, types, functions, and other elements and make changes to them. At the time of writing, reflection is not supported by the main branches of compilers, but it is available in branches such as clang [2] and gcc, which can be build on user's own. For quick experiments, one can also use Compiler Explorer [A]. In review articles and videos on this topic, the most common example is printing arbitrary structures or serializing them in JSON. In this article, the author invites the reader to practice with a slightly more complex example -deserialization with protobuf. Overview Section Goals and Objectives outlines the goals and objectives, while the Instrumentarium section provides a brief overview of the reflection features that will be needed for our exercise. Section Protobuf briefly describes the serialization format. In the Design section, we will formulate the design rationales. In the Implementation section, we will write the code, and in Verification, we will outline the testing methodology. Section Conclusions will summarize the results of our exercises. The Discussion section will supplement the details regarding the intentions or motivations behind some aspects of the design and implementation. Goals and Objectives The purpose of this article is to write a solution for deserializing arbitrary messages in protobuf format with the possibility of using them in projects without dynamic memory (malloc-free). The main goal is to master new C++ features and familiarize readers with examples of their practical use. A secondary goal is to understand the Protobuf serialization format. Instrumentarium ^^expr the reflection operator - prefix ^^ - produces a reflection value from a grammatical construct - its operand expr. Reflection values — reflections for short - values of an opaque type std::meta::info, which one can use in metafunctions to analyze or alter properties of grammar constructs. [1] [: refl:] splicer - produces an expression evaluating to the entity represented by r in grammatical contexts that permit expressions For example [A] int main() { int x = 1; constexpr auto r = ^^x; return [: r :]; } nonstatic_data_members_of returns a vector of reflections representing the direct non-static members of the class type represented by its first argument. In some cases its result can be used only after transforming to a static array with function define_static_array [3]. access_context is a non-aggregate type that represents a namespace, class, or function from which queries pertaining to access rules may be performed, as well as the designating class. access_context::unchecked function, returning access_context with no access restrictions. For example [B] struct foo { int x; }; int main() { foo bar {12}; constexpr auto refl = define_static_array(nonstatic_data_members_of(^^foo, access_context::unchecked()))[0]; return bar.[: refl :]; } type_of returns a metaobject, representing type of the reflection, passed as its parameter. extract<type> returns value, associated with the reflection, passed as its parameter. template for is an expansion statement [4], unrolling the loop at compile time, repeating its body for all elements of the argument. Important difference from plain for is that the type of the cycle variable is evaluated for each iteration. This allows iterating over tuple elements. For example [C] template for (auto i : make_tuple(0, 'a')) { println("{}", i); } =vannotation is a compile-time value that can be associated with a construct to which attributes can appertain [5]. Annotations differs from attributes by presence of ‘=’ prefix. Annotations allow values of a user type, provided the values are constexpr. annotations_of returns all the annotations on item. For example [D] enum [[=11]] foo {}; int main() { constexpr auto bar = annotations_of(^^foo)[0]; return extract<int>(bar); } data_member_spec returns a reflection of a data member description for a data member of given type. define_aggregate takes the reflection of an incomplete class/struct/union type and a range of reflections of data member descriptions and completes the given class type with data members as described consteval { stmt; } designates a scope that allows calling reflection functions with side effects, such as define_aggregate For example [E] struct foo; consteval { define_aggregate(^^foo, { data_member_spec(^^int, {"value"}) }); } int main() { foo bar { 10 }; return bar.value; } Protobuf Protobuf (Protocol Buffers) is a data serialization format [6] from Google for encoding structured data into a compact binary format for exchanging data (messages) between different systems. The message format is described in a .proto file [7], from which C++ (or other) code for serialization and deserialization can then be generated. Protobuf supports basic field types (arithmetic, string, boolean), user-defined types (enumerations and structures), sequences (repeated), unions (oneof), and maps (map). Integer types explicitly specify their bit width (int32, int64) and subtype fixed or signed (fixed32, sint64). Fixed types are stored with a fixed number of bytes, while other types are stored with a variable, minimum-required number of bytes. In binary format, fields are identified by their numbers from proto and type, and can follow in any order or be omitted [8]. Although in proto2 fields can be marked as required, this is not recommended (required: Do not use), and in proto3 all fields are optional. Design Rationales Decisions The Protobuf message type for C++ code will be created or translated by the user using C++ data structures, supported data types, and annotations Field numbers will be assigned by the user by adding an annotation of type int with the field number For scalar types and Protobuf data subtypes, we will define analogues - enum with corresponding base types int32, sint32, uint32, fixed32, sfixed32, int64, sint64, uint64, fixed64, sfixed64 We will also support some native C++ types: bool, int32_t, int64_t, uint32_t, uint64_t, float, and double, which will represent Protobuf types bool, sint32, sint64, uint32, uint64, float, and double, respectively For Protobuf enumeration types, the user will define analogues in C++, and our solution will allow their use in message structures. Protobuf character strings will be supported using std::string, char[N], or std::array<char,N> at the user's discretion. We will support nested messages as nested structures or as std::unique_ptr to such structures For repeated fields, we will support static arrays and containers with the push_back method - std::vector, std::list For all types except containers, we will allow the use of std::optional wrappers Protobuf map - std::map or std::unordered_map at the user's choice oneof - anonymous union or std::variant at the user's choice Packed sequences will also be marked with the packed annotation We will use std::istream as the input data stream. We will ignore unrecognized or excessive elements in the input stream. To deserialize fields, we will use a dispatch table with functions for reading message fields. We will fill the table using reflection for all message fields. The result of the message deserialization function will be a set of flags indicating the presence of values in the fields Definition Based on the decisions made, we can make the following definitions: A field is a non-static member of a structure with a number in the annotation. A message is a structure (class) that has at least one field. Principle of operations The user creates a message structure or translates it from proto. The user code calls the deserialize function with the input stream and an instance of the message structure. At compile time, our code fills in a dispatch table with functions for reading fields. During deserialization, our code For each element in the input stream, tries all field reading functions until it succeeds If the element is not recognized, it skips it Upon completion of deserialization, returns a set of flags as an indication of the presence of fields Translation example Protobuf C++ enum Enum { NOTOK = 0; OK = 42; } message Example { sint32 signed_int = 1; uint32 unsigned_int = 2; fixed32 fixed_32 = 3; string text = 4; Enum status = 5; repeated double array_of_double = 6; map<sint32, string> map_int_to_text = 7; oneof Union { sint64 int_alternative = 8; double double_alternative = 9; } } enum class Enum { NOTOK = 0, OK = 42 }; struct Example { sint32 signed_int [[=1]]; uint32 unsigned_int [[=2]]; fixed32 fixed_32 [[=3]]; string text [[=4]]; Enum status [[=5]]; vector<double> array_of_double [[=6]]; map<sint32, string> map_int_to_text [[=7]]; union { sint64 int_alternative [[=8]]; double double_alternative [[=9]]; }; }; Implementation Skeleton of the message deserialization function Implementing the decisions made in the previous section and abstracting from the details, let's write the skeleton of the deserialization function “in the first approximation”: template<message Message> auto deserialize(std::istream &input, Message &msg) { // Filling dispach table at compile time static constexpr auto field_readers = make_field_readers<Message>(); deserialize_result_type<Message> result { }; while (input.good()) { // While reading is possible const auto [num, type] = read_id_type(input); // reading data type and id if (! input.good()) break; deserialize_result_type<Message> field_success { }; for (auto reader : field_readers) { // Trying to apply every item from the dispatch table if (const auto attempt_success = reader(input, msg, num, type); attempt_success) { field_success = attempt_success; // Stop trying on success break; } } if (field_success) { result |= field_success; } else { skip(input, type); } } return result; } The functions and types used here (message, make_field_readers, deserialize_result_type, read_id_type, skip) are details that we have abstracted away and will gradually implement in the following sections. An online example of this function can be found at [F]. So far, this is standard C++ code and does not require any features from C++26. The dispatch table generation function make_field_readers make_field_readers template function generates field reading methods by iterating over all fields of a message and generating a reading function for each one, via instantiating the field_reader template function (example [G]). Here, the splice operator ([:field:]) will come in handy. In order to use it in a loop, the field variable must be constexpr, which is only possible in the template for extension operator. Accordingly, the extension statement argument must be static, i.e. the fields_of function must use define_static_array, just like make_field_readers. template<message Message> consteval auto make_field_readers() { std::vector<field_reader_type<Message>> result {}; template for(constexpr auto field : fields_of<Message>()) { result.push_back(field_reader<field, num_of(field)>); } return std::define_static_array(result); } Field reader function field_reader This template function checks whether the received field number type matches the expected one, and if they match, calls the deserialize function for this field type. Several deserialization types are expected, so our deserialize will be overloaded, and one of the overloads will be the message deserialization function we wrote earlier. Function num_of This function finds and returns the first annotation of integer type (int) in the input reflection parameter. To implement it, we will use annotations_of to get a list of annotations, type_of to find out the data type of the annotation, and extract<int> to get the value (example [H]). consteval auto num_of(std::meta::info member) { auto annotations = std::meta::annotations_of(member); auto found = std::ranges::find_if(annotations, [](auto annotation) { return std::meta::type_of(annotation) == ^^int; } ); return found == annotations.end() ? invalid_num : std::meta::extract<int>(*found); } Function fields_of This function returns a list of fields of the class passed as a template parameter (example [H]). Here, the reflection operator ^^Class and nonstatic_data_members_of are used to obtain a list of non-static data members and filter them by the presence of a number in the annotation. template<class Class> consteval auto fields_of() { constexpr auto ctx = std::meta::access_context::unchecked(); std::vector<std::meta::info> fields{}; template for(constexpr auto member : define_static_array(nonstatic_data_members_of(^^Class, ctx))) { if constexpr (num_of(member) != invalid_num ) fields.push_back(member); } return std::define_static_array(fields); } Concept message Now we have enough means to define message concept. template<class Message> concept message = std::is_class_v<Message> && (fields_of<Message>().size() != 0); Deserialization result type deserialize_result_type We will create this type as a set of bit fields, each of which will correspond to a field in the message. To make it easier to use, we will make the names of the bit fields identical to the names of the message fields. To do this, we will use define_aggregate and data_member_spec to create a definition for our incomplete type deserialize_result_type (example [I]), and identifier_of to get the message field identifier. template<message Message> struct deserialize_result_type; template<typename Message> consteval auto define_result() { return define_aggregate(^^deserialize_result_type<Message>, std::views::transform(fields_of<Message>(), [](auto field) { return data_member_spec(^^bool, {identifier_of(field), {}, 1U}); })); } To complete the definition, this function should be called in a consteval block. struct foo { int num1 [[=1]]; long num2 [[=2]]; }; consteval { define_result<foo>(); } This is a workable solution, but it puts responsibility to call define_result for each message type on the user. To make this happen automatically, as needed, this consteval block must be in a context where Message is present as a parameter, i.e., in an auxiliary template structure. And in order for field_reader to return a result with a bit corresponding to the read field, we need a function that would set this bit according to the reflection value of the field. Let's do all this in an auxiliary template (example [J]). template<message Message> struct deserialize_helper { struct result_type; static consteval auto define_result() { return std::meta::define_aggregate(^^result_type, std::views::transform(fields_of<Message>(), [](auto field) { return std::meta::data_member_spec(^^bool, {std::meta::identifier_of(field), {}, 1U}); })); } consteval { define_result(); } static consteval result_type set_by_name_of(auto Info) { struct result_type result {}; template for(constexpr auto bit : members_of<result_type>()) { if (identifier_of(Info) == identifier_of(bit)) { result.[:bit:] = true; } } return result; } template<std::meta::info Member> requires (std::same_as<Message, class_of<Member>>) static constexpr result_type deserialized() { template for(constexpr auto field : fields_of<class_of<Member>>()) { if constexpr(field == Member) { return set_by_name_of(field); } } return {}; } }; template<message Message> using deserialize_result_type = deserialize_helper<Message>::result_type; template<std::meta::info Field> constexpr auto deserialized() { return deserialize_helper<class_of<Field>>::template deserialized<Field>(); } (Un)packed sequences Packed Protobuf sequences mean that their elements will follow one after another, which saves on field numbers. Elements of unpacked sequences can be mixed with other fields, each preceded by a field number. Sequences of primitive types are packed by default, but can be marked as unpacked. To mark them as such, we define the data type: enum class packed { default_, packed, unpacked }; Users will use this data type to denote unpacked sequences of scalar types: struct foo { int unp[2] =1, =packed::unpacked; long pack[2] =2, =packed::packed; long norm[2] [[=3]]; }; To use this attribute in our code, let's define a structure with proto attributes supported in our implementation (only packed for now): struct attributes { packed packed; }; And the function of reading attributes using reflection (example [K]): consteval auto attrs_of(std::meta::info member) { return std::ranges::fold_left(std::meta::annotations_of(member), attributes { }, [](attributes attrs, auto annotation) { if (type_of(annotation) == ^^packed) attrs.packed = std::meta::extract<packed>(annotation); return attrs; }); } We will use the obtained attributes in the field_reader function This concludes the need for reflection; from this point on our implementation will only use old good C++23. Serialization of primitive types Protobuf uses variable-bit-width encoding [7], in which an input stream byte contains seven informative bits and one service bit, whose zero value indicates that it is a terminal byte, and non-zero value indicates that it is an intermediate byte. To read variable data, we will write a function: inline std::uint64_t read_variant(std::istream &input) { static constexpr char high = 0x80; std::uint64_t val { }; char chr { }; unsigned count = 0; for (bool done = false; !done && input.get(chr).good(); done = (chr & high) != high, count += 7) { val |= std::uint64_t(chr & 0x7F) << count; } return val; } For signed integers (signed int), zigzag encoding is also used to reduce the number of non-zero high bits. Zigzag can be decoded using the following function: template<typename T> constexpr auto decode_zigzag(std::uint64_t value) noexcept { return static_cast<T>((value >> 1) ^ (-(value & 1))); } One of the features of protobuf is the ability to skip a data element based solely on data from the input stream. The need to skip something may arise due to the presence of new fields in the input stream that have no counterparts in the local message type definitions. The function for skipping such elements may look like this: inline auto skip(std::istream &input, data_type type) { switch (type) { case data_type::fixed32: input.ignore(sizeof(fixed32)); break; case data_type::fixed64: input.ignore(sizeof(fixed64)); break; case data_type::variant: read_variant(input); break; case data_type::lengthy: input.ignore(read_variant(input)); } } Data types and concepts Let's define data types corresponding to types in Protobuf enum int32 : std::int32_t {}; enum sint32 : std::int32_t {}; enum uint32 : std::uint32_t {}; enum fixed32 : std::uint32_t {}; enum sfixed32 : std::int32_t {}; enum int64 : std::int64_t {}; enum sint64 : std::int64_t {}; enum uint64 : std::uint64_t {}; enum fixed64 : std::uint64_t {}; enum sfixed64 : std::int64_t {}; group them by serialization methods using concepts: template<template<typename A, typename B> class Pred, typename A, typename ... B> consteval auto anyof() {return std::disjunction_v<Pred<A, B>...>;} template<typename Type> concept enumeration = std::is_enum_v<Type> && !anyof<std::is_same, Type, int32, sint32, uint32, fixed32, sfixed32, int64, sint64, uint64, fixed64, sfixed64>(); template<typename Type> concept variant_integral = anyof<std::is_same, Type, bool, std::uint32_t, int32, uint32, std::uint64_t, int64, uint64>() || enumeration<Type>; template<typename Type> concept zigzag_integral = anyof<std::is_same, Type, std::int32_t, sint32, std::int64_t, sint64>(); template<typename Type> concept fixed_arithmetic = anyof<std::is_same, Type, fixed32, sfixed32, fixed64, sfixed64, float, double>(); and write deserialization functions for each of these concepts: template<variant_integral Type> void deserialize(std::istream& input, Type& value) { value = static_cast<Type>(read_variant(input)); } void deserialize(std::istream& input, fixed_arithmetic auto& value) { input.read(static_cast<char*>(static_cast<void*>(&value)), sizeof(value)); } template<zigzag_integral Type> void deserialize(std::istream& input, Type& value) { value = decode_zigzag<Type>(read_variant(input)); } We will fill dynamic arrays using push_back, resize, and move elements, so let's define the relevant concepts: template<typename Container> concept resizable = requires(Container c) { c.resize(1); }; template<typename Container> concept back_insertable = requires(Container container) { container.push_back(std::declval<typename Container::value_type>()); }; and concept of an associative container: template<typename Container> concept associative = requires(Container c, Container::key_type k, Container::mapped_type v) { c[k] = v; }; Static arrays do not satisfy back-insertable concept, so we define bounded_array concept: template<typename Array> concept bounded_array = (std::is_bounded_array_v<Array> && elementary<std::remove_extent_t<Array>>) || (std::ranges::output_range<Array, typename Array::value_type> && !resizable<Array> && elementary<typename Array::value_type>); For each of these concepts, we will write a deserialize function. Oneof support At the time of writing, the version of clang with reflection support did not allow anonymous unions [9] to be accessed as regular fields. Instead, it required reflection on the anonymous union itself: obj.[:outer:].[:Field:]. To workaround this, we will add a separate union_reader function. A fields of type std::variant needs numbers for each alternative, and the reader needs to call to emplace<Index>() before accessing the alternative. To do this, we will also write a separate function, oneof_reader. This will slightly complicate the make_field_readers function, which now has to choose one of three reading functions. Nested messages The length of nested messages is recorded before the message data, and the read function must not exceed this length. However, for the root message the length is not provided, and the read function is limited only by the length of the data stream. To implement these features without repeating the body of the deserialize function, let's add a parameter — a pointer to the function that will read the length of nested messages. For root messages, we will pass a pointer to the unlimited function, which will simply return the max_length constant. The final version of our deserialization solution is presented in example [L]. Verification The protoc compiler allows serializing data for any message described in the proto format [10]. protoc --encode=tests.numeric.SInt_32 numeric.proto Let's use this capability to prepare test vectors for functional tests of our solution. Unfortunately, the compilation time limits set on compiler-explorer allow only a small number of tests to be compiled online. The offline solution contains 400+ tests and will be available on github. Conclusions Reflection in C++26, even in its initial form, is a powerful tool that will allow us to do without external code generation in many applications that are currently impossible without it. Reflection tools are fairly easy to understand and learn, and their use will reduce boilerplate code, simplify serialization and deserialization implementations, and enhance metaprogramming capabilities. Discussion Is a dispatch table necessary? All functions that operate on reflection must be executed in a consteval context. A constexpr dispatch table guarantees such a context and is fairly simple to create and use. The author admits that there may be solutions without such a table. Between reflection and the type system Reflection functions can solve all tasks on types that were previously performed, for example, by template specialization. However, the author was unable to find a simple solution to replace specialization. template<typename> struct member_traits; template<class Class, typename Type> struct member_traits<std::optional<Type> Class::*> { using value_type = Type; }; Perhaps template specialization is and will remain a simpler solution than writing functions in the reflection space. Subtype differentiation protobuf defines several subtypes for integer types - fixed32, sfixed32, sint32, int32, uint32, and similarly for 64-bit types. The subtype affects how the value is serialized. For example, fixed32/sfixed32 are always encoded in 4 bytes, others use variable encoding, and sint32/sint64 also use zigzag encoding. We could be possible define corresponding aliases for subtypes and use the reflection operator's property to return different info objects for different aliases, but these types appear as de-aliased in class definitions. For these experiments, the author used simple enums. However, it should be noted that enum “breaks” standard type property queries, such as numeric_limits, is_integral, is_signed, is_unsigned, etc. Only some of them can be specialized (numeric_limits), so using message structures with such field types in other templates or libraries may be difficult. Indicators of value presence Since all message fields are optional, we need to decide how to inform the client which fields have been read and which have not. One option is to use std::optional. However, this can lead to unwanted “bloating” of the structure. Another option is to return a set of flags from the deserialization operation as an indication of the presence/absence of a field. However, implementing this approach for nested structures can be non-trivial and inconvenient to use. The default field value can also indicate the absence of a value, since protobuf skips fields with default values during serialization, with the exception of packed arrays. This article implements support for std::optional and a set of flags for the root message. oneof The protobuf documentation specifies that the fields combined with oneof are deserialized as if they were placed directly in the message. This corresponds to anonymous unions in C++. However, unions have certain safety issues. Therefore, in this solution, the author also implemented support for std::variant with the corresponding count of numbers in the annotations: std::variant<int, double, std::string> oneof [[=4, =5, =6]]; Building clang with reflection The clang sources with reflection support is available at [2]. The author used the following commands to build the compiler: cmake -S llvm -B build -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi;libunwind" cmake --build build --parallel 5 protoc The protobuf compiler is available as a standard package on Linux builds. On Ubuntu, it can be installed with the command: sudo apt-get install protobuf-compiler Possible development Validation of message types for non-uniqueness of field numbers presence of more than one integer annotation per field, mismatch between the number of field numbers and the number of alternatives in the variant, etc. Serialization Abstraction from iostream List of references Reflection for C++26 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2996r13.html LLVM fork for P2996 https://github.com/bloomberg/clang-p2996 define_static_{string,object,array} https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3491r3.html Expansion Statements https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p1306r5.html Annotations for Reflection https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3394r4.html Protocol Buffers https://uk.wikipedia.org/wiki/Protocol_Buffers Language Guide (proto 3) https://protobuf.dev/programming-guides/proto3/ Encoding https://protobuf.dev/programming-guides/encoding/ Reflection of an anonymous union member is not giving expected results https://github.com/bloomberg/clang-p2996/issues/251 Protocol buffer compiler https://manpages.debian.org/testing/protobuf-compiler/protoc.1.en.html List of online examples https://compiler-explorer.com/z/511q4rPPY https://compiler-explorer.com/z/ccaxa79WY https://compiler-explorer.com/z/aePE7Wvhh https://compiler-explorer.com/z/hjG56Wqoj https://compiler-explorer.com/z/43aaeMK4q https://compiler-explorer.com/z/dqv4874Ee https://compiler-explorer.com/z/sEh8T7qcj https://compiler-explorer.com/z/vqdarbqMe https://compiler-explorer.com/z/Teesnh4db https://compiler-explorer.com/z/nrn4srWfs https://compiler-explorer.com/z/hGv5azh69 https://compiler-explorer.com/z/oPcTEh9vv
Вступ Нова редакція стандарту C++ [1], випуск якої очікується у нинішньому, 2026 році, додасть в C++ рефлексію (reflection). Це дозволить розробникам писати код, який досліджуватиме властивості класів, типів, функцій, та інших елементів і робити у них зміни. На момент написання статті, рефлексія хоча і не підтримується основними гілками компіляторів, проте доступна у відгалуженнях як clang […]
Вступ Нова редакція стандарту C++ [1], випуск якої очікується у нинішньому, 2026 році, додасть в C++ рефлексію (reflection). Це дозволить розробникам писати код, який досліджуватиме властивості класів, типів, функцій, та інших елементів і робити у них зміни. На момент написання статті, рефлексія хоча і не підтримується основними гілками компіляторів, проте доступна у відгалуженнях як clang [2] так і gcc, які можна зібрати самостійно. Для експериментів нашвидкоруч можна також скористатись Compiler Explorer [A]. У оглядових статтях та відео на цю тему найпоширеніший приклад - друкування довільних структур чи їх серіалізація в JSON. В рамках цієї статті автор пропонує читачу повправлятися разом з автором із дещо складнішим прикладом - десеріалізацією із protobuf. Огляд Розділ Цілі та Мета окреслить мету і цілі, розділ статті Інструментарій дасть коротку довідку по фічах рефлексії, що знадобляться для нашої вправи. Розділ Protobuf коротко опише формат сералізації. У розділі Дизайн зформулюємо дизайн рішення. У розділі Імплементація ми напишемо код, а у Верифікація окреслимо методологію тестування. Розділі Підсумки узагальнить результати наших вправ. Розділ Дискусія доповнить деталями щодо намірів чи мотивації деяких аспектів дизайну та імплементації. Цілі та Мета Ціль цієї статті - написати рішення для десеріалізації довільних повідомлень у форматі protobuf з можливістю використання в проєктах без динамічної пам’яті (malloc-free). Основна мета - освоїти нові інструменти C++ та ознайомити читачів із прикладами їх практичного використання. Побічна мета - розібратися із форматом серіалізації Protobuf. Інструментарій ^^expr оператор рефлексії - префікс ^^ - обраховує і повертає значення рефлексії (метаоб’єкта) для заданого операнду expr. Значення рефлексії, або просто рефлексія - це значення непрозорого типу даних std::meta::info,яке можна використати у метафункціях для аналізу чи генерації граматичних конструкцій. [: refl:] Граматична конструкція зрощення (англ. splice) робить зворотну дію - генерує граматичну конструкцію з рефлексії. Наприклад [A] int main() { int x = 1; constexpr auto r = ^^x; return [: r :]; } nonstatic_data_members_of повертає колекцію метаоб’єктів, що описують не статичні члени даних структури, рефлексія якої передана як аргумент. В деяких випадках, для використання її результату в контекстах часу компіляції його необхідно перетворити в статичний масив функцією define_static_array [3]. access_context клас, що репрезентує контекст - неймспейс, клас, чи функцію, з якого виконуються запити, пов’язані і правами доступу до класу, що є предметом запиту. access_context::unchecked функція що повертає контекст без обмежень прав доступу. Наприклад [B] struct foo { int x; }; int main() { foo bar {12}; constexpr auto refl = define_static_array(nonstatic_data_members_of(^^foo, access_context::unchecked()))[0]; return bar.[: refl :]; } type_of повертає метаоб’єкт, що описує тип елементу, рефлексію якого переданого в якості аргументу. extract<type> повертає значення, асоційоване із метаоб’єктом, переданим в якості аргументу. template for оператор розширення [4], що розгортає цикл під час компіляції, повторюючи тіло циклу для всіх елементів аргументу циклу. Важливою відмінністю від звичайного for є те що тип змінної циклу, як і всіх змінних в його тілі, визначається для кожної ітерації окремо. Це дозволяє, наприклад, ітерацію по елементам кортежу (tuple). Наприклад [C] template for (auto i : make_tuple(0, 'a')) { println("{}", i); } =v анотації - це значення часу компіляції [5], асоційовані із синтаксичною конструкцією, для якої дозволені атрибути. Анотації відрізняються від атрибутів префіксом ‘=’. Вони дозволяють використання користувацьких типів за умови constexpr-есивності їх значень. annotations_of повертає колекцію метаоб’єктів, що описують значення анотацій, асоційованих із аргументом Наприклад [D] enum [[=11]] foo {}; int main() { constexpr auto bar = annotations_of(^^foo)[0]; return extract<int>(bar); } data_member_spec повертає метаоб’єкт, що описує новий член даних. define_aggregate додає члени даних до заданого, незавершеного класу/структури/об'єднання і робить його завершеним. consteval { stmt; } позначає блок у якому дозволено виконати мета-функції із стороннім ефектом, як от define_aggregate Наприклад [E] struct foo; consteval { define_aggregate(^^foo, { data_member_spec(^^int, {"value"}) }); } int main() { foo bar { 10 }; return bar.value; } Protobuf Protobuf, (Protocol Buffers) - це формат серіалізації даних [6] від Google для кодування структурованих даних в компактний бінарний формат для обміну даними (повідомленнями) між різними системами. Формат повідомлень описується у файлі .proto [7], з якого потім можна згенерувати C++ (чи який інший) код для серіалізації і десеріалізації. Protobuf підтримує базові типи полів (арифметичні, строкові, булеві), користувацькі (енумерації та структури), послідовності (repeated), об’єднання (oneof), та мапи (map). Цілі типи однозначно вказують їхню розрядність (int32, int64) та підтип fixed чи signed (fixed32, sint64). Fixed типи записуються фіксованою кількістю байт а інші типи - варіативною, мінімально-необхідною кількістю байт. У бінарному форматі поля позначаються їхніми номерами із proto та типом, і можуть слідувати у довільному порядку чи бути пропущеними [8]. І хоча в редакції proto2 поля можна позначити як required проте це робити не рекомендується (required: Do not use), а в proto3 усі поля опціональні. Дизайн Рішення Тип повідомлень Protobuf для C++ коду створюватиме або транслюватиме користувач, використовуючи структури даних C++, підтримувані типи даних, та анотації Номери полів призначатиме користувач, додаючи анотацію типу int із номером поля Для скалярних типів і підтипів даних Protobuf визначимо аналоги - enum із відповідними базовими типами int32, sint32, uint32, fixed32, sfixed32, int64, sint64, uint64, fixed64, sfixed64 Підтримаємо також деякі нативні типи C++: bool, int32_t, int64_t, uint32_t, uint64_t, float та double, що репрезентуватимуть Protobuf типи bool, sint32, sint64, uint32, uint64, float, та double відповідно Для енумераційних типів Protobuf користувач визначатиме аналогічні в C++, а наше рішення дозволить їх використання в структурах повідомлень. Рядки символів Protobuf string - підтримаємо з використанням std::string, char[N] чи std::array<char,N> на вибір користувача Вкладені повідомлення підтримуємо як вкладені структури чи як std::unique_ptr на такі структури Для полів послідовностей (repeated) підтримаємо статичні масиви та контейнери з методом push_back - vector, list Для усіх типів, окрім контейнерів, дозволимо використання обгорток std::optional Protobuf map - std::map або std::unorderd_map на вибір користувача oneof - анонімне об'єднання (union) або std::variant на вибір користувача Упаковані послідовності (packed) також позначатимемо анотацією packed В якості вхідного потоку даних використаємо std::istream Нерозпізнані та надлишкові елементи вхідного потоку - ігноруємо Для десеріалізації полів використаємо диспетч-таблицю із функціями для читання полів повідомлення Таблицю заповнюємо з використанням рефлексії для усіх полів повідомлення Результат функції десеріалізації повідомлення матиме набір прапорців, як індикацію наявності значень у полях Визначення Згідно до прийнятих рішень, можемо зробити наступні визначення: поле - це не статичний член структури із номером в анотації повідомлення - це структура (клас) що має хоча б одне поле Принцип роботи Користувач створює структуру повідомлення, чи транслює її з proto Користувацький код викликає функцію deserialize із вхідним потоком і екземпляром структури повідомлення Наш код під час компіляції заповнює диспетч-таблицю із функціями для читання полів В процесі десеріалізації наш код Для кожного елементу у входному стрімі пробує усі функції читання полів поки не досягне успіху якщо елемент не розпізнали - пропускає його по завершенню десеріалізації повертає набір флагів, як індикацію наявності полів Приклад трансляції Protobuf C++ enum Enum { NOTOK = 0; OK = 42; } message Example { sint32 signed_int = 1; uint32 unsigned_int = 2; fixed32 fixed_32 = 3; string text = 4; Enum status = 5; repeated double array_of_double = 6; map<sint32, string> map_int_to_text = 7; oneof Union { sint64 int_alternative = 8; double double_alternative = 9; } } enum class Enum { NOTOK = 0, OK = 42 }; struct Example { sint32 signed_int [[=1]]; uint32 unsigned_int [[=2]]; fixed32 fixed_32 [[=3]]; string text [[=4]]; Enum status [[=5]]; vector<double> array_of_double [[=6]]; map<sint32, string> map_int_to_text [[=7]]; union { sint64 int_alternative [[=8]]; double double_alternative [[=9]]; }; }; Імплементація Скелет функції десеріалізації повідомлення Втілюючи рішення, прийняті у попередньому розділі, та абстрагуючись від деталей, напишемо скелет функції десеріалізації “у першому підході”: template<message Message> auto deserialize(std::istream &input, Message &msg) { // Під час компіляції заповнюємо диспетч-таблицю static constexpr auto field_readers = make_field_readers<Message>(); deserialize_result_type<Message> result { }; while (input.good()) { // Поки можна читати const auto [num, type] = read_id_type(input); // Читаємо номер поля і тип if (! input.good()) break; deserialize_result_type<Message> field_success { }; for (auto reader : field_readers) { // Пробуємо застосувати кожен елемент із таблиці if (const auto attempt_success = reader(input, msg, num, type); attempt_success) { field_success = attempt_success; // Якщо успішно прочитали, завершуємо спроби break; } } if (field_success) { result |= field_success; } else { skip(input, type); } } return result; } Функції і типи щи тут використовуються (message, make_field_readers, deserialize_result_type, read_id_type, skip) - це ті деталі, від яких ми абстрагувались і які поступово напишемо у наступних розділях. Онлайн приклад цієї функції можна знайти за посиланням [F]. Поки що це звичний C++ код, ніяких фіч із C++26 не потребує. Функція генерації диспетч-таблиці make_field_readers Функція-шаблон make_field_readers для генерації методів читання полів проходиться по всіх полях повідомлення і для кожного генерує функцію читання, підставляючи параметри у шаблонну функцію field_reader (приклад [G]). Тут нам стане у нагоді оператор зрощення ([:field:]). А щоб його можна було використати у циклі, змінна field має бути constexpr, що можливо лише у операторі розширення template for. Відповідно список розширення має бути статичним, тобто функція fields_of має використати define_static_array, так само як і make_field_readers template<message Message> consteval auto make_field_readers() { std::vector<field_reader_type<Message>> result {}; template for(constexpr auto field : fields_of<Message>()) { result.push_back(field_reader<field, num_of(field)>); } return std::define_static_array(result); } field_reader_type<Message> - це тип вказівника на функцію field_reader Функція читача поля field_reader Ця шаблонна функція перевіряє відповідність отриманого типу номера поля відносно очікуваного, і якщо збігаються, викликає функцію deserialize для типу цього поля. Типів десеріалізації очікується декілька, то ж наша deserialize буде перевантажена, і одним із перевантажень буде написана нами раніше функція десеріалізації повідомлення. template<std::meta::info Field, int Num> deserialize_result_type<class_of<Field>> field_reader(std::istream &input, class_of<Field> &obj, int num, data_type type) { using Type = data_type_of<Field>; if (num == Num && matches<Type>(type)) { deserialize(input, obj.[:Field:]); return deserialized<Field>(); } return {}; } Функція num_of Ця функція знаходить і повертає першу анотацію цілого типу (int) у вхідному параметрі-рефлексії. Для її написання використаємо annotations_of, щоб отримати список анотацій, type_of - щоб дізнатись тип даних анотації, та extract<int> - для отримання значення (приклад [H]). consteval auto num_of(std::meta::info member) { auto annotations = std::meta::annotations_of(member); auto found = std::ranges::find_if(annotations, [](auto annotation) { return std::meta::type_of(annotation) == ^^int; } ); return found == annotations.end() ? invalid_num : std::meta::extract<int>(*found); } Функція fields_of Ця функція повертає список полів класу, переданого як параметр шаблону (приклад [H]). Тут використовується оператор рефлексії ^^Class, та nonstatic_data_members_of, щоб отримати перелік не статичних членів даних і відфільтрувати їх за наявністю номера в анотації. template<class Class> consteval auto fields_of() { constexpr auto ctx = std::meta::access_context::unchecked(); std::vector<std::meta::info> fields{}; template for(constexpr auto member : define_static_array(nonstatic_data_members_of(^^Class, ctx))) { if constexpr (num_of(member) != invalid_num ) fields.push_back(member); } return std::define_static_array(fields); } Концепція message Тепер ми маємо достатньо засобів, щоб визначити концепцію message template<class Message> concept message = std::is_class_v<Message> && (fields_of<Message>().size() != 0); Тип результату десеріалізації deserialize_result_type Цей тип створимо, як набір бітових полів, кожне з яких відповідатиме полю у повідомленні. А щоб було простіше користуватись, імена бітових полів зробимо тотожними іменам полів повідомлення. Для цього скористаємось define_aggregate та data_member_spec щоб створити визначення для нашого незавершеного типу deserialize_result_type (приклад [I]), та identifier_of щоб отримати ідентифікатор поля повідомлення. template<message Message> struct deserialize_result_type; template<typename Message> consteval auto define_result() { return define_aggregate(^^deserialize_result_type<Message>, std::views::transform(fields_of<Message>(), [](auto field) { return data_member_spec(^^bool, {identifier_of(field), {}, 1U}); })); } Для завершення визначення, цю функцію слід викликати в consteval блоці struct foo { int num1 [[=1]]; long num2 [[=2]]; }; consteval { define_result<foo>(); } Таке рішення робоче, проте воно покладає на користувача обов’язок викликати define_result для кожного типу повідомлення. Щоб це відбулося автоматично, за потребою, цей consteval блок має бути у контексті, де Message присутній як параметр, тобто допоміжній шаблонній структурі. А щоб field_reader міг повернути результат з бітом, що відповідає прочитаному полю, потрібна функція, яка б встановлювала цей біт по значенню рефлексії поля. Зробимо це все у допоміжному шаблоні (приклад [J]) template<message Message> struct deserialize_helper { struct result_type; static consteval auto define_result() { return std::meta::define_aggregate(^^result_type, std::views::transform(fields_of<Message>(), [](auto field) { return std::meta::data_member_spec(^^bool, {std::meta::identifier_of(field), {}, 1U}); })); } consteval { define_result(); } static consteval result_type set_by_name_of(auto Info) { struct result_type result {}; template for(constexpr auto bit : members_of<result_type>()) { if (identifier_of(Info) == identifier_of(bit)) { result.[:bit:] = true; } } return result; } template<std::meta::info Member> requires (std::same_as<Message, class_of<Member>>) static constexpr result_type deserialized() { template for(constexpr auto field : fields_of<class_of<Member>>()) { if constexpr(field == Member) { return set_by_name_of(field); } } return {}; } }; template<message Message> using deserialize_result_type = deserialize_helper<Message>::result_type; template<std::meta::info Field> constexpr auto deserialized() { return deserialize_helper<class_of<Field>>::template deserialized<Field>(); } (Не)Упаковані послідовності Упакованість послідовностей Protobuf означає, що її елементи будуть слідувати один за одним, що дозволяє зекономити на номері поля. Елементи неупакованих послідовностей можуть іти в перемішку із іншими полями, кожному із них передує номер поля. Послідовності примітивних типів упаковані по замовчуванню, проте можуть бути позначені як неупаковані. Для такого позначення визначимо тип даних enum class packed { default_, packed, unpacked }; Цей тип даних користувачі використовуватимуть для позначення не упакованих послідовностей скалярних типів: struct foo { int unp[2] =1, =packed::unpacked; long pack[2] =2, =packed::packed; long norm[2] [[=3]]; }; Щоб використати цей атрибут у нашому коді, визначимо структуру із атрибутами proto, що підтримуються в нашій імплементації (поки що тільки packed) struct attributes { packed packed; }; Та функцію вичитування атрибутів засобами рефлексії (приклад [K]): consteval auto attrs_of(std::meta::info member) { return std::ranges::fold_left(std::meta::annotations_of(member), attributes { }, [](attributes attrs, auto annotation) { if (type_of(annotation) == ^^packed) attrs.packed = std::meta::extract<packed>(annotation); return attrs; }); } Отримані атрибути використаємо в функції field_reader На цьому потреби в використанні рефлексії вичерпані, далі наша імплементація використовуватиме лише старий добрий C++23. Серіалізація примітивних типів В Protobuf використовується кодування з варіативною бітністю [7], у якому байт вхідного потоку містить сім інформативних біт і один службовий, нульове значення якого вказує що це термінальний байт, а не нульове, відповідно, на проміжний. Для читання варіативних даних напишемо функцію inline std::uint64_t read_variant(std::istream &input) { static constexpr char high = 0x80; std::uint64_t val { }; char chr { }; unsigned count = 0; for (bool done = false; !done && input.get(chr).good(); done = (chr & high) != high, count += 7) { val |= std::uint64_t(chr & 0x7F) << count; } return val; } До цілих типів зі знаком (signed int) застосовується ще й зигзаг кодування, щоб зменшити кількість ненульових старших біт. Декодувати зигзаг можна такою функцією: template<typename T> constexpr auto decode_zigzag(std::uint64_t value) noexcept { return static_cast<T>((value >> 1) ^ (-(value & 1))); } Однією із особливостей protobuf є можливість пропустити елемент даних базуючись лише на даних із вхідного потоку. Необхідність щось пропустити може виникнути через наявність нових полів у вхідному потоці, що не мають відповідників у локальних дефініціях типів повідомлень. Функція для пропуску таких елементів може мати такий вигляд: inline auto skip(std::istream &input, data_type type) { switch (type) { case data_type::fixed32: input.ignore(sizeof(fixed32)); break; case data_type::fixed64: input.ignore(sizeof(fixed64)); break; case data_type::variant: read_variant(input); break; case data_type::lengthy: input.ignore(read_variant(input)); } } Типи даних і концепції Визначимо типи даних відповідники типів у Protobuf enum int32 : std::int32_t {}; enum sint32 : std::int32_t {}; enum uint32 : std::uint32_t {}; enum fixed32 : std::uint32_t {}; enum sfixed32 : std::int32_t {}; enum int64 : std::int64_t {}; enum sint64 : std::int64_t {}; enum uint64 : std::uint64_t {}; enum fixed64 : std::uint64_t {}; enum sfixed64 : std::int64_t {}; Згрупуємо їх по способам серіалізації за допомогою концепцій: template<template<typename A, typename B> class Pred, typename A, typename ... B> consteval auto anyof() {return std::disjunction_v<Pred<A, B>...>;} template<typename Type> concept enumeration = std::is_enum_v<Type> && !anyof<std::is_same, Type, int32, sint32, uint32, fixed32, sfixed32, int64, sint64, uint64, fixed64, sfixed64>(); template<typename Type> concept variant_integral = anyof<std::is_same, Type, bool, std::uint32_t, int32, uint32, std::uint64_t, int64, uint64>() || enumeration<Type>; template<typename Type> concept zigzag_integral = anyof<std::is_same, Type, std::int32_t, sint32, std::int64_t, sint64>(); template<typename Type> concept fixed_arithmetic = anyof<std::is_same, Type, fixed32, sfixed32, fixed64, sfixed64, float, double>(); Та напишемо функції десеріалізації для кожної із цих концепцій template<variant_integral Type> void deserialize(std::istream& input, Type& value) { value = static_cast<Type>(read_variant(input)); } void deserialize(std::istream& input, fixed_arithmetic auto& value) { input.read(static_cast<char*>(static_cast<void*>(&value)), sizeof(value)); } template<zigzag_integral Type> void deserialize(std::istream& input, Type& value) { value = decode_zigzag<Type>(read_variant(input)); } Заповнення динамічних масивів будемо робити з використанням push_back, resize, та move елементів, то ж визначимо відповідні концепції: template<typename Container> concept resizable = requires(Container c) { c.resize(1); }; template<typename Container> concept back_insertable = requires(Container container) { container.push_back(std::declval<typename Container::value_type>()); }; концепцію back-insertable контейнера template<typename Container> concept container = back_insertable<Container> && (scalar<typename Container::value_type> || (std::is_class_v<typename Container::value_type> && elementary<typename Container::value_type>)); та концепцію асоціативного контейнера template<typename Container> concept associative = requires(Container c, Container::key_type k, Container::mapped_type v) { c[k] = v; }; Оскільки статичні масиви не відповідають концепції back-insertable контейнера, визначимо концепцію обмеженого масиву: template<typename Array> concept bounded_array = (std::is_bounded_array_v<Array> && elementary<std::remove_extent_t<Array>>) || (std::ranges::output_range<Array, typename Array::value_type> && !resizable<Array> && elementary<typename Array::value_type>); Для кожної із цих концепцій напишемо функцію deserialize Підтримка oneof Версія clang з підтримкою рефлексії на момент написання статті не дозволяла звертатись до полів анонімних об’єднань [9], як до звичайних полів. Натомість потребувала ще й рефлексію на саме анонімне об’єднання: obj.[:outer:].[:Field:]. Щоб обійти це, додамо окрему функцію union_reader. Що до полів типу std::variant, то такому полю потрібен номер на кожну альтернативу, та виклик emplace<Index>() перед доступом до альтернативи. Для цього теж напишемо окрему функцію oneof_reader. Це, відповідно, трохи ускладнить і функцію make_field_readers, що тепер має вибирати одну із трьох функцій читання. template<message Message> consteval auto make_field_readers() { std::vector<field_reader_type<Message>> result {}; template for(constexpr auto field : fields_of<Message>()) { if constexpr(is_variant_type(type_of(field))) { constexpr auto nums = nums_of(field); constexpr unsigned n = std::min(nums.size(), template_arguments_of( type_of(field)).size()); template for(constexpr auto i : std::views::iota(0u, n)) { result.push_back(oneof_reader<field, nums[i], i>); } } else if constexpr(is_union_type(parent_of(field)) && ! has_identifier( parent_of(field))) { result.push_back(union_reader<field, num_of(field)>); } else { result.push_back(field_reader<field, num_of(field)>); } } return std::define_static_array(result); } Вкладені повідомлення Довжина вкладених повідомлень, записується перед даними повідомлення, і функція читання не має вийти за межі цієї довжини. В той час як для кореневого повідомлення довжина не записується і функція читання обмежена лише довжиною потоку даних. Щоб імплементувати ці особливості не повторюючи тіло функції deserialize, додамо параметр - вказівник на функцію що читатиме довжину вкладених повідомлень. Для кореневих повідомлень передамо вказівник на функцію unlimited, яка просто поверне константу max_length. Остаточна версія нашого рішення десеріалізації подана а прикладі [L]. Верифікація Компілятор protoc дозволяє серіалізувати дані для довільного повідомлення описаного в форматі proto [10]. protoc --encode=tests.numeric.SInt_32 numeric.proto Скористаємось цією можливістю для підготовки тестових векторів для функціональних тестів нашого рішення. На жаль, обмеження часу компіляції, встановлені на compiler-explorer, дозволяють скомпілювати онлайн лише незначну кількість тестів. Офлайн рішення містить 400+ тестів і буде доступно на github. Пiдсумки Рефлексія в C++26, навіть у її початковому вигляді, доволі потужний інструмент, що дозволить обійтись без зовнішніх кодогенерацій у багатьох із тих застосувань що нині неможливі без таких. Інструменти рефлексії доволі прості для розуміння і освоєння, і їх використання зменшить кількість boilerplate коду, спростить імплементації серіалізації та десеріалізації, та посилить можливості метапрограмування. Дискусія Чи потрібна диспетч таблиця? Всі функції, що оперують над рефлексією, мають бути виконані в consteval контексті. constexpr диспетч таблиця гарантує такий контекст і є доволі простою у створенні і використанні. Автор допускає що можуть бути рішення і без такої таблиці. Між рефлексією та системою типів Функціями рефлексії можна вирішити усі завдання над типами, що раніше виконувалися, наприклад, спеціалізацією шаблонів. Однак автору не вдалось знайти простого рішення для заміни спеціалізації template<typename> struct member_traits; template<class Class, typename Type> struct member_traits<std::optional<Type> Class::*> { using value_type = Type; }; Можливо спеціалізація шаблону є, і залишиться простішим рішенням, ніж написання функцій в просторі рефлексій. Диференціація підтипів protobuf визначає кілька підтипів для цілих типів - fixed32, sfixed32, sint32, int32, uint32 і аналогічно для 64 бітних. Підтип впливає на те, як серіалізується значення. Наприклад fixed32/sfixed32 завжди кодуються в 4 байти, інші використовують варіативне кодування, а sint32/sint64 ще й зигзаг кодування. Можна було б визначити відповідні аліаси для підтипів і скористатись властивістю оператора рефлексії повертати відмінні об’єкти info для різних аліасів, проте у дефініції класів ці типи з’являються як де-аліасед. Для цих експериментів автор використав прості енуми. Проте слід враховувати що enum “ламає” стандартні запити властивостей типів, як от numeric_limits, is_integral, is_signed, is_unsigned, то що. Тільки деякі із них можна спеціалізувати (numeric_limits), то ж використання структур повідомлень з такими типами полів в інших шаблонах чи бібліотеках може бути ускладнене. Індикатори наявності значень Оскільки усі поля повідомлення опціональні, слід вирішити як повідомляти клієнту які полі прочитали а які - ні. Один із варіантів використовувати std::optional. Проте це може призвести до небажаного “роздування” структури. Інший варіант - повертати із операції десеріалізації набір флагів як індикацію наявності/відсутності поля. Однак реалізація такого підходу для вкладених структур може виявитись нетривіальною і не зручною у використанні. Значення поля за замовчуванням також може бути індикацією відсутності значення, оскільки protobuf при серіалізації пропускає поля із значенням по замочуванню, за винятком упакованих масивів. В цій статті реалізовано підтримка std::optional та набір флагів для кореневого повідомлення. oneof Документація protobuf визначає що поля, об’єднані oneof, десеріалізуються так, ніби вони були розміщені безпосередньо у повідомленні. Це відповідає анонімним об’єднанням в C++. Проте об’єднання мають певні проблеми із безпечністю використання. Тому в цьому рішенні автор також реалізував ще і підтримку std::variant із відповідною кількістю номерів в анотаціях: std::variant<int, double, std::string> oneof [[=4, =5, =6]]; Збирання clang з рефлексією Сирці clang з підтримкою рефлексії доступні за посиланням [2]. Автор використовував такі команди, щоб зібрати компілятор cmake -S llvm -B build -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi;libunwind" cmake --build build --parallel 5 protoc Компілятор protobuf доступний як стандартний пакет на Linux збірках. На Ubuntu його можна інсталювати командою sudo apt-get install protobuf-compiler Можливий розвиток Валідація типів повідомлень на предмет не унікальності номера поля наявності більше ніж однієї цілої анотації на поле, невідповідність кількості номерів полів кількості альтернатив у варіанті, тощо Серіалізація Абстрагування від iostream Перелік посилань Reflection for C++26 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2996r13.html LLVM fork for P2996 https://github.com/bloomberg/clang-p2996 define_static_{string,object,array} https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3491r3.html Expansion Statements https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p1306r5.html Annotations for Reflection https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3394r4.html Protocol Buffers https://uk.wikipedia.org/wiki/Protocol_Buffers Language Guide (proto 3) https://protobuf.dev/programming-guides/proto3/ Encoding https://protobuf.dev/programming-guides/encoding/ Reflection of an anonymous union member is not giving expected results https://github.com/bloomberg/clang-p2996/issues/251 Protocol buffer compiler https://manpages.debian.org/testing/protobuf-compiler/protoc.1.en.html Перелік онлайн прикладів коду https://compiler-explorer.com/z/511q4rPPY https://compiler-explorer.com/z/ccaxa79WY https://compiler-explorer.com/z/aePE7Wvhh https://compiler-explorer.com/z/hjG56Wqoj https://compiler-explorer.com/z/43aaeMK4q https://compiler-explorer.com/z/dqv4874Ee https://compiler-explorer.com/z/sEh8T7qcj https://compiler-explorer.com/z/vqdarbqMe https://compiler-explorer.com/z/Teesnh4db https://compiler-explorer.com/z/nrn4srWfs https://compiler-explorer.com/z/hGv5azh69 https://compiler-explorer.com/z/oPcTEh9vv
Off-target testing embedded projects with STUBMMIO
Introduction Off-target testing - building and running tests "on a development host or build server rather than on the target hardware" . [3] stubmmio is a C++ library for off-target embedded unit tests by stubbing MCU’s MMIO regions on a Linux machine. Memory-mapped I/O (MMIO) is a method of performing I/O through registers, mapped to […]
Introduction Off-target testing - building and running tests "on a development host or build server rather than on the target hardware" . [3] stubmmio is a C++ library for off-target embedded unit tests by stubbing MCU’s MMIO regions on a Linux machine. Memory-mapped I/O (MMIO) is a method of performing I/O through registers, mapped to MCU’s address space [1]. stubmmio allows the users to substitute MMIO registers of interest with the host memory, setup the initial state of the stubbed MMIO registers, and verify their state against expected after running the code under test. Principle of Operation An off-target unit test: Assumes the code under test (CUT) has been compiled for the host and is callable from a C++ code Reserves MMIO address space (test arena) in the host process memory Maps MMIO-in-use with mmap Setups MMIO-in-use at the state, predefined by the running test case Runs CUT Validates MMIO state stubmmio provides classes to support this approach: stubmmio::stub to map the MMIO-in-use and setting up initial state stubmmio::verify to validate MMIO state stubmmio::stimulus to simulate simple asynchronous behaviour of the peripheral CUT Constraints Off-target embedded UT with stubmmio imposes the following constraints: CUT compiles for the host CUT is callable from C++ (e.g. written in C or C++) CUT does not have an infinite loop CUT does not relay or precise timing CUT does not expect complicated behaviour of the hardware Primary Classes stubmmio::stub stubmmio::stub is a collection of memory elements. A memory element is a continuous memory range staring at a given address. The stub memory elements usually have initial data. When a stub is applied, it allocates memory pages to for all its elements and writes initial element data. On destruction, stub deallocates all pages. Multiple stubmmio::stub instances are allowed, provided they refer to different host memory pages. A stubmmio::stub instance owns the allocated pages, so that other instances cannot use pages, owned by this instance. stubmmio::verify stubmmio::verify is a collection of memory elements. Its elements are used to compare the elements’ data with the memory state. stubmmio::verify ensures that every of its element refers to a page, previously allocated by a stubmmio::stub instance. stubmmio::stimulus stubmmio::stimulus is a pair MMIO addresses, associated with condition and action. After stubmmio::stimulus instantiation, the condition is asynchronously monitored. When its condition satisfied, the action is executed. The main purpose of stubmmio::stimulus is to simulate simple hardware behaviour, essential for the CUT to complete its operations. stubmmio::stub::initializer_list and stubmmio::verify::initializer_list These initializer_list classes facilitate composition of stub and vefify instances from pieces, shared among multiple tests of a test suite. Unit Test Framework stubmmio does not imply any particular UT framework. For its own tests it uses boost::ut. The users are free to use a C++ UT framework of their choice. Off-Target Build Aspects, associated with compilation of CUT for the host are not in scope of this project. Address Space stubmmio is suitable for testing code for 8-, 16-, and 32-bit target MCU's, which have address space in ranges up to 00000000-FFFFFFFF. Off-target UT are expected to be built for a x86_64 Linux machine, which has plenty of unused address space. However, depending on the compiler in use, this layout may overlap with MMIO layout. Reserving the Test Arena The easiest way to reserve a suitable address space layout is to enable PIE and ASLR with compiler options ‑fPIE ‑pie. This effectively moves memory layout of the executable in the upper address space, making the entire 00000000-FFFFFFFF range available for mmap. However, in cases when MCU MMIO addresses are provided via a linker script (such as MSP430 projects), ASLR also randomizes those MMIO addresses. So, the UT executable should be linked with -Wl,-no-pie -Wl,-Ttext-segment=0x60000000, which reserves 00000000-5FFFFFFF range. In all cases static linking is recommended, any third-party shared libraries better be avoided. Using Address Range 0000-FFFF The first 64K are restricted for use by a kernel tunable and can be allowed with vm.mmap_min_addr set to zero [2]. Access to Unmapped Memory When CUT attempts to access MMIO, not backed with the host memory, the UT process expectingly terminates with SIGSEGV, leaving remaining tests not run. stubmmio provides an easy way for converting SIGSEGV into exception with stubmmio::util::handle_sigsegv(). The exception is then captured with the UT framework, marking the corresponding tests as failed, and the test execution can be continued. Examples Simple example using namespace stubmmio; // Declare the initial memory setup const stub setup { // TB0CTL is a periferal registers {&TB0CTL, 0U} }; // Declare the expected memory state const verify expected { {&TB0CTL, TBSSEL__SMCLK | MC__CONTINUOUS}, }; setup(); // Activate the stub Timer_init(); // Run CUT expect(expected()); // Verify expected memory state Example with a stimulus using namespace stubmmio; // USIC0_CH1, VADC and VADC_G1 are pereifal regsiters stub state_on_reset { {USIC0_CH1, USIC_CH_StateOnReset()}, // Initialize USIC0_CH1 with data returned by USIC_CH_StateOnReset() {VADC, VADC_StateOnReset()}, // Initialize VADC with data returned by VADC_StateOnReset() {VADC_G1, VADC_G_NotReady()}, // Initialize VADC_G1 with data returned by VADC_StateOnReset() }}; const verify state_after_test = { { &SCU_GENERAL->PASSWD, 0x000000C3U }, { &SCU_CLK->CGATCLR0, SCU_CLK_CGATCLR0_VADC_Msk }, { &VADC->CLC, 0U }, { &VADC->GLOBCFG, uint32_t(VADC_GLOBCFG_SUCAL_Msk | VADC_StateOnReset().GLOBCFG) }, { &VADC_G1->RCR[7], VADC_G_RCR_WFR_Msk }, { &VADC->BRSSEL[1], VADC_BRSSEL_CHSELG7_Msk }, { &VADC->BRSMR, ((0x01UL << VADC_BRSMR_ENGT_Pos) | VADC_BRSMR_SCAN_Msk | VADC_BRSMR_LDEV_Msk) }, { &VADC_G1->ARBCFG, VADC_G_ARBCFG_ANONC_Msk }, { &VADC_G1->ARBPR, VADC_G_ARBPR_ASEN2_Msk } }; state_on_reset(); // Activate the stub stimulus adc_ready { // Simulates assertion of ADC ready flag upon CUT clearing VADC_CLC_DISR flag &VADC->CLC, [](const volatile uint32_t& clc) { return (clc & VADC_CLC_DISR_Msk) == 0; }, &VADC->CLC, [](volatile uint32_t& clc) { clc &= ~VADC_CLC_DISS_Msk; } }; stimulus adcg_ready { // Simulates assertion of ADCG ready flag upon CUT setting VADC_G_ARBPR_ASEN2 flag &VADC_G1->ARBPR, [](const volatile uint32_t& arbpr) { return (arbpr & VADC_G_ARBPR_ASEN2_Msk) != 0; }, &VADC_G1->ARBCFG, [](volatile uint32_t& arbcfg) { arbcfg &= ~VADC_G_ARBCFG_CAL_Msk; } }; ADC_Init(); // Run CUT expect(state_after_test()); // Verify expected memory state References Memory-mapped I/O and port-mapped I/O https://en.wikipedia.org/wiki/Memory-mapped_I/O_and_port-mapped_I/O mmap_min_addr, Debian Wiki https://wiki.debian.org/mmap_min_addr Unit Testing For Embedded Software Development https://dojofive.com/blog/unit-testing-for-embedded-software-development/
Logovod – macro-free allocation-free user-configurable logger front-end for C++
1. Features logovod features Three styles of use — with functions, ostream left shifts, format string Automatic space insertion of functional style Automatic printing of standard containers, tuples, variants, optional Compile or run-time priority levels Compile or run-time sinks Constrained message length Support for wide-char streams Priority levels according to RFC-5424 section-6.2.1 2. Concerns The […]
1. Features logovod features Three styles of use — with functions, ostream left shifts, format string Automatic space insertion of functional style Automatic printing of standard containers, tuples, variants, optional Compile or run-time priority levels Compile or run-time sinks Constrained message length Support for wide-char streams Priority levels according to RFC-5424 section-6.2.1 2. Concerns The following conerns were identified Easy of use prevails of precise formatting A macro-free logger front-end is desirable, as macros: may trigger/expose unintended side-effects may hide build errors until a particular log-level is enabled discouraged by ISOCPP Allocation in the logger may induce bad alloc errors Unconstrained log message size accompanied with C++ operators may cause unintentionally huge data dumps Troubleshooting race conditions requires sync-free logging Log level limits should be available for build-time and run-time Sink selection should be available for build-time and run-time Logger tuning should be flexible enough to focus on a particular problem and void unrelated records Logger front-end should introduce minimal possible penalties and overhead Changing log level should not impact any possible side-effects of the user’s logging statements Logger’s formatter should not interfere with application’s formatters Access to the logger is needed from different contexts, such as static methods, free functions, lambdas. A shared instance of a logger front-end would require some synchronization technique In some use cases the log messages should be attributed with some common information, such as date, tag, source location, thread-id, process id. Some log sinks automatically attribute the messages, some others don’t In some cases log messages should be routed to more than one sink. This multi-sink routing may need some logic based on the message attributes Such sinks may have different approach or capabilities on message attributing Logger from-end is the only source of source location information An application should not require any run-time configuration file to use logger run-time configuration file is an option, not a prerequisite Some libraries needs logging, however, where to log and whether tog should be decided by application Shared logger configuration is better for consistency, ad-hoc overrides – for flexibility Some standard to string conversions are available only for operator<<(std::ostream,...) . Different teams prefer different syntax of the log statements – function calls, stream left shift operators, format string 3. Design 3.1. Design Philosophy Focus on a simple ready-to-use solution and provide easy-to-use means for making custom solutions as complex as needed 3.2. Design Decisions Logger configuration responsibilities are assigned to concept of log Category. Log Category defines logger properties and controls it behavior Logger is a template class parametrized by a log category Log emitters are inner classes of the logger template, additionally parametrized by a log priority level Log sink is a set of weakly related concepts – prologer, writer, epiloger. prologer, writer, epiloger are free functions. Any state needed for their operations maintained externally, for example in variables with static storage class A category may redefine sink concepts as a functor object, if needed An emitter: buffers data writes, issued by the user, collects message attributes, calls prologer, epiloger and flushes the message to the writer An emitter is considered to be a disposable, one-time use object. Design, however, should not disallow other use cases, such as emitter reuse, or deferred use. Emitter supports three style of use: invoke , left shift and format string Emitter deploys std::ostream for converting value to text Emitter uses internally allocated stream buffer of capacity, defined in the category If category defines unlimited capacity, basic_stringbuf is used 4. Quick Start Guide // Instantiate a logger type using Log = logovod::logger<logovod::category>; // Emit log messages with a function-call style Log::debug("Hello Logovod"); // or with I/O stream shifts Log::info{} << "Hello Logovod I/O stream style"; // or with std::format Log::notice{}.format("Hello {} format style", "Logovod"); 5. User Guide Please visit this github page User Guide 6. Sources Logovod sources are shared via github https://github.com/hutorny/logovod 7. License Logovod is licensed under MIT
is_constexpr – a template to test if a function is constexpr
C++ allows declaring functions as constexpr. Such functions could be evaluated at compile time and used, for example in if constexpr(...). However, a non-constexpr function in this context is not allowed. So, if we want to write a template, accepting a function and using it in either if constexpr(...) or old plain if(...) we need […]
C++ allows declaring functions as constexpr. Such functions could be evaluated at compile time and used, for example in if constexpr(...). However, a non-constexpr function in this context is not allowed. So, if we want to write a template, accepting a function and using it in either if constexpr(...) or old plain if(...) we need to test whether the function is constexpr, otherwise compile fails as illustrated below: template<auto F> bool is_zero() { if constexpr(F() == 0) { // error: constexpr if condition is not a constant expression return true; } else { return false; } } int notzero() { return 1; } constexpr int zero() { return 0; } bool test() { auto z1 = is_zero<notzero>(); // in instantiation of function template requested here auto z2 = is_zero<zero>(); return z1 || z2; } So we need to rewrite our is_zero as the following ... template<auto F> bool is_zero() { if constexpr(is_constexpr<F>::value) { if constexpr(F() == 0) { return true; } else return false; } else { return F() == 0; } } ... and implement is_constexpr using SFINAE: // is_constexpr.h template<auto F> struct constexpr_true : std::true_type {}; template<auto F> static auto test_constexpr(int) -> constexpr_true<F()>; template<auto F> static auto test_constexpr(long) -> std::false_type; template<auto F> struct is_constexpr : decltype(test_constexpr<F>(0)) {}; is_constexpr template has a constexpr member value, that can be used, for example in static_assert or if constexpr, as illustarted in the example below #include "is_constexpr.h" int notzero() { return 1; } constexpr int zero() { return 0; } static_assert(!is_constexpr<notzero>::value); static_assert(is_constexpr<zero>::value); template<auto F> bool is_zero() { if constexpr(is_constexpr<F>::value) { if constexpr(F() == 0) { return true; } else return false; } else { return F() == 0; } } bool test() { return is_zero<zero>() || is_zero<notzero>(); } Live is_constexpr example at godbolt.org
setof – an iterable set of strongly typed enumerations
This post suggests an implementation of template class setof which facilitates features of a bit set and a forward-iterable container.
C++11 has introduced class enum - strongly scoped and strongly typed enumerations with class enum keywords. For example we may declare states of our FSA as the following: class enum state { shutdown, initialization, running, maintenance, finalization, }; However, using class enum to create masks is quite inconvenient: constexpr unsigned allowed = (1<<static_cast<int>(state::running)) | (1<<static_cast<int>(state::maintenance)) std::bitset does not help either on this matter. To address this case, a template class setof is suggested (also available on Gist setof.hpp) This class supports bitset type of operations & | &= |= set reset and facilitates an iterator over populated bits. #include <type_traits> #if __cplusplus > 201709L #include <bit> template<typename T> constexpr int countr_zero(T val) noexcept { return std::countr_zero(val); } template<typename T> constexpr int popcount(T val) noexcept { return std::popcount(val); } #else template<typename T, typename = std::enable_if_t<std::is_integral<T>::value,T>> constexpr int countr_zero(T val) noexcept { return __builtin_ctz(val); } template<typename T, typename = std::enable_if_t<std::is_integral<T>::value,T>> constexpr int popcount(T val) noexcept { return __builtin_popcount(val); } #endif template<typename T, typename = std::enable_if_t<std::is_integral<T>::value,T>> constexpr int bitwidth(const T&) noexcept { return sizeof(T) * 8; } template<template<std::size_t> class T, std::size_t N, typename = decltype(&T<N>::_Find_first)> constexpr int countr_zero(const T<N>& val) noexcept { return val._Find_first(); /* _Find_first is not in standard, its GCC extension */ } template<template<std::size_t> class T, std::size_t N> constexpr int popcount(const T<N>& val) noexcept { return val.count(); } template<template<std::size_t> class T, std::size_t N> constexpr int bitwidth(const T<N>& val) noexcept { return val.size(); } /** * setof - set of "named" bits, e.g. bit made from an enum * params: * Enum - enum type * Storage - storage type, integral or bitset<N> */ template<typename Enum, typename Storage = unsigned> class setof { public: static constexpr Storage lsh(Enum value) noexcept { return static_cast<Storage>(1) << static_cast<unsigned>(value); } static constexpr Storage bit_or() noexcept { return {}; } template<typename ... Enums> static constexpr Storage bit_or(Enum value, Enums ... values) noexcept { if constexpr (sizeof...(values) == 0 ) return lsh(value); else return lsh(value) | bit_or(values...); } template<typename ... Enums> constexpr setof(Enums ... values) noexcept : bits { bit_or(values...) } { static_assert(std::conjunction<std::is_same<Enum,Enums>...>::value, "Value type mismatch"); } constexpr setof(const setof& that) noexcept : bits { that.bits } {} constexpr setof operator&(const setof& that) const noexcept { return setof(bits & that.bits); } constexpr setof operator|(const setof& that) const noexcept { return setof(bits | that.bits); } constexpr setof& operator|=(const setof& that) noexcept { bits |= that.bits; return *this; } constexpr setof& operator&=(const setof& that) noexcept { bits &= that.bits; return *this; } constexpr setof& operator=(const setof& that) noexcept { bits = that.bits; return *this; } constexpr bool operator[](Enum val) const noexcept { return (bits & static_cast<unsigned>(val)) != Storage{}; } constexpr bool operator==(const setof& that) const noexcept { return bits == that.bits; } explicit constexpr operator Storage () const noexcept { return bits; } constexpr int capacity() const noexcept { return bitwidth(bits); } template<typename ... Enums> constexpr bool any(Enums ... values) const noexcept { static_assert(std::conjunction<std::is_same<Enum,Enums>...>::value, "Value type mismatch"); return Storage {} != (bits & setof(values...).bits); } template<typename ... Enums> constexpr bool all(Enums ... values) const noexcept { static_assert(std::conjunction<std::is_same<Enum,Enums>...>::value, "Value type mismatch"); return setof(values...).bits == (bits & setof(values...).bits); } template<typename ... Enums> constexpr bool none(Enums ... values) const noexcept { static_assert(std::conjunction<std::is_same<Enum,Enums>...>::value, "Value type mismatch"); return Storage {} == (bits & setof(values...).bits); } constexpr bool all(const setof& value) const noexcept { return value.bits == (bits & value.bits); } constexpr bool any(const setof& value) const noexcept { return Storage {} != (bits & value.bits); } constexpr bool none(const setof& value) const noexcept { return Storage {} == (bits & value.bits); } constexpr bool empty() const noexcept { return bits == Storage {}; } constexpr auto count() const noexcept { return popcount(bits); } setof& set(Enum val) noexcept { bits |= lsh(val); return *this; } setof& reset(Enum val) noexcept { bits &= ~lsh(val); return *this; } class iterator { public: constexpr iterator(const iterator&) = default; constexpr iterator& operator=(const iterator&) = default; constexpr Enum operator*() const noexcept { return static_cast<Enum>(pos); } constexpr iterator& operator++() { const auto rem = container.bits >> ++pos; if( rem == Storage{} ) pos = container.capacity(); else pos += countr_zero(rem); return *this; } constexpr bool operator==(const iterator& that) const noexcept { return &container == &that.container && pos == that.pos; } constexpr bool operator!=(const iterator& that) const noexcept { return not (that == *this); } private: friend class setof; constexpr iterator(const setof& cont) noexcept : container(cont), pos {cont.empty() ? cont.capacity() : countr_zero(cont.bits)} {} constexpr iterator(const setof& cont, unsigned) noexcept : container(cont), pos {cont.capacity()} {} const setof& container; int pos; }; constexpr iterator begin() const noexcept { return iterator(*this); } constexpr iterator end() const noexcept { return iterator(*this,0); } private: constexpr setof(Storage val) : bits { val } {} Storage bits; }; Usage Creating a set of enums //Our enum enum class Code { a, b, c, d, e, f, g }; // constexpr set constexpr auto fixed = setof<Code>(Code::a, Code::c, Code::d); // variable set setof<Code> variable(Code::c, Code::d); // result of a binary operation auto combined = fixed | variable; Altering a set variable |= Code::g; // adding individual value combined.set(Code::f); variable.reset(Code::a); // removing individual value Querying a set // Querying for listed values if( variable.any(Code::c, Code::e) ) { // .... } // Querying for another set if( combined.all(fixed) ) { // .... } Iterating for(auto i : combined) { // ... } Specifying underlying storage By default, setof uses unsigned data type for storing bit mask. This can be opted by providing another data type as the second parameter: auto small = setof<Code,uint8_t>(Code::a, Code::c, Code::d); auto llong = setof<Code,uint64_t>(Code::c, Code::d); // Storage type can by integral setof<Code,std::bitset<1024>> vlong(Code::c, Code::d); // or bitset Notes std::bitset based setof iterator relays on bitset::_Find_first, which is not in the standard and supported by GCC only.
C++ does not allow using string literals as case values in a switch statement. In modern C++ this restriction can be worked around with constexpr hash values. E.g. instead of using string literals, we will be using their hash codes, computed during compilation and matched agains a hash code of a varable string. First we […]
C++ does not allow using string literals as case values in a switch statement. In modern C++ this restriction can be worked around with constexpr hash values. E.g. instead of using string literals, we will be using their hash codes, computed during compilation and matched agains a hash code of a varable string. First we define a hash function: #pragma once #include <cstdint> #include <string_view> namespace fnv1b { /* FNV-1b hash function with extra rotation * https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function */ template<typename T = std::size_t> inline T hash(const char* str, std::size_t size) noexcept; template<typename T> inline constexpr T ror(T x, unsigned N) noexcept { return (x << (sizeof(T)*8 - N)) | ((x) >> (N)); } template<> inline constexpr uint32_t hash<uint32_t>(const char* str, std::size_t size) noexcept { constexpr uint32_t val32 = 0x811c9dc5; constexpr uint32_t prime32 = 16777619; uint32_t result = val32; while(size--) { result ^= uint32_t(*str); result = ror(result, (8+(0xF&(*str++)))); /* added rotation 8-23 bits */ result *= prime32; } return result; } template<> inline constexpr uint64_t hash<uint64_t>(const char* str, std::size_t size) noexcept { constexpr uint64_t val64 = 0xcbf29ce484222325; constexpr uint64_t prime64 = 1099511628211ULL; uint64_t result = val64; while(size--) { result ^= uint64_t(*str); result = ror(result, (8+(*str++)%48)); /* added rotation 8-55 bits */ result *= prime64; } return result; } template<typename T = std::size_t> inline constexpr T hash(const std::string_view str) noexcept { return hash<T>(str.data(), str.size()); } } Also available on gist For easy of use we define a literal operator, computing hash from a string literal: constexpr uint32_t operator "" _h(const char* str, size_t size) noexcept { return fnv1b::hash<uint32_t>(str, size); } Usage #include <iostream> #include <string> #include "hash_fnv1b.hpp" using namespace std; int main() { auto s = "hello"; switch(fnv1b::hash<uint32_t>(string_view(s))) { case "world"_h: cout << "World"; break; case "hello"_h: cout << "Hello"; break; } cout << endl; } This example is aslo available live at https://onlinegdb.com/BJydy3XvN
This simple C++17 template implements constexpr any_of for matching a value against a list of constants. /** type_of_list<...>::type - helper structure for determining type of the first item in the list */ template<auto ... List> struct type_of_list; template<auto List> struct type_of_list<List> { using type = decltype(List); }; template<auto First, auto ... List > struct type_of_list<First, […]
This simple C++17 template implements constexpr any_of for matching a value against a list of constants. /** type_of_list<...>::type - helper structure for determining type of the first item in the list */ template<auto ... List> struct type_of_list; template<auto List> struct type_of_list<List> { using type = decltype(List); }; template<auto First, auto ... List > struct type_of_list<First, List...> : type_of_list<First> {}; /** type_of<...>::type - determines type of the first item in the list */ template<auto ... List> using type_of = typename type_of_list<List...>::type; /** any_of<...>(value) - compares value against all template parameters and returns true if any matches */ template<auto ... List> inline constexpr bool any_of(type_of<List...> value) noexcept { return ((value == List) || ...); } Also available on github as a gist any_of.hpp Usage #include <iostream> #include <string> enum class num { zero, one, two }; int main() { num v { num::one }; std::cout << any_of<num::zero, num::two>(v) << " " << any_of<num::zero, num::one>(v) << std::endl; } This example is also available on https://www.onlinegdb.com/BJLvFo7PN
COJSON library is also available as Arduino Studio .ZIP library: cojson.zip. To add the library to your sketch, download cojson.zip, select: Sketch > Include Library > Add .ZIP Library ... The library sources are available in a separate repository on github: cojson.lib
C++17 has simplified use of template parameters. Instructions, given in this tutorial, describes how to define JSON model with cojson::autos templates. Note: GCC 7 or higher is required to compile cojson_autos.hpp ! 1. Defining Model JSON model is defined as a template function, with pointers to all serializable members as the template parameters. Everything else […]
C++17 has simplified use of template parameters. Instructions, given in this tutorial, describes how to define JSON model with cojson::autos templates. Note: GCC 7 or higher is required to compile cojson_autos.hpp ! 1. Defining Model JSON model is defined as a template function, with pointers to all serializable members as the template parameters. Everything else - JSON property name, data type, array extents, is deduced from pointer-to-member by cojson::autos templates. cojson::autos defines two groups of template functions each having its specific purpose and name: O<class,...>() Object<class, ...>() Defines an object mapped to a C++ class P<auto,auto>() Property<auto,auto>() Defines an object property, bound to getter/setter or to an inner object struct MyClass { int Int; } obj; O<MyClass,&MyClass::Int>().read(obj, in); 1.1. Objects Class-bound objects are defined with variadic template O<...> that accepts bounding class as the first parameter followed by the list of member pointers, method pointers or P templates. 1.2. Names cojson::autos templates deduce names from the identifiers. E.g. member Int will be mapped to JSON property "Int". In a case, when this approach is not suitable, a cojson::P template should be used to define a property with an alternate name. 2. Scalar properties 2.1. Members A class's member is referred by its pointer to member, e.g. &MyClass::Int 2.2. Methods A property, mapped to a single getter or setter is referred by its pointer to method, e.g. &MyClass::setLatch. A property, that needs two methods - getter and setter, is defined by cojson::autos::P template, e.g. P<&MyClass::getLatch, &MyClass::setLatch>. 2.3. Immutable String Immutable zero-terminated strings of unspecified length can be provided: by a pointer to (const) member of const char* by a pointer to method returning const char* by template PropertyStaticString with the class and a pointer to static member of const char* by template PropertyFunctionString with the class and a pointer to static method returning const char* struct MyClass { const char* const a = "a"; const char* b() const noexcept { return "b"; } static const char* const c; static const char* d() noexcept { return "d"; } }; O<MyClass, &MyClass::a, &MyClass::b, PropertyStaticString<MyClass, &MyClass::c>, PropertyFunctionString<MyClass, &MyClass::d> >(); 2.4. Mutable String Members Mutable strings can be bound to a string of predefined capacity (char[N]) by a pointer to member. std::string by a pointer to member. #include <string> #include <cojson_stdlib.hpp> //support for std::string properties struct MyClass { char msg[32]; std::string str; }; O<MyClass, &MyClass::msg, &MyClass::str>(); 3. Nested Objects Nested objects are mapped to a member of a class that has its own json model by using P template with pointer to member and pointer to method or function returning its model. struct Outer { bool Bool; struct Inner { int Int; } obj; }; O<Outer, &Outer::Bool, P<&Outer::obj, O<Outer::Inner, &Outer::Inner::Int>>>(); 4. Arrays 4.1. Vectors - arrays of integral values Vectors are bound by a pointer to member of an array type or std::vector type #include <vector> #include <cojson_stdlib.hpp> //support for std::vector properties struct MyClass { int vector[3]; std::vector<int> stdvect; }; O<MyClass, &MyClass::vector, &MyClass::stdvect>(); 4.2. Strings - arrays of string values String arrays are bound by a pointer to member of a two-dimensional array type (char[N][M]) or to a std::vector<std::string> struct MyClass { int strings[3][32]; std::vector<std::string> stdstrings; }; O<MyClass, &MyClass::strings, &MyClass::stdstrings>(); 4.3. Arrays of Objects Similarly to nested objects, arrays of objects are mapped to an array member (or std::vector) of a class that has its own json model by using P template with pointer to member and pointer to method or function returning its model. struct Outer { bool Bool; struct Inner { int Int; } objs[4]; std::vector<Inner> Objs; }; O<Outer, &Outer::Bool, P<&Outer::objs, O<Inner, &Inner:Int>>>(); O<Outer, &Outer::Bool, P<&Outer::Objs, O<Inner, &Inner:Int>>>(); 4.4. Lists - arrays of heterogeneous values cojson is designed to work with a predefined structure and this design imposes certain limitation on arrays - a heterogeneous array (ordered list) must have each of its item defined, fixed to a given type and bound to a given member. A list is defined with PropertyList template. struct MyClass { int list; bool next; char last[32]; }; Object<MyClass, PropertyList<&MyClass::list, &MyClass::next, &MyClass::last>>(); This will correspond to the following json: {"list":[0, false, ""]} 5. Reading/Writing 5.1. Via std::iostream or similar cojson::wrapper namespace provides template wrappers for C++ streams: istream and ostream, for reading and writing respectively. They require the wrapped streams to provide get(char) good() eof() / put(char) good() and can wrap almost any of the stdlib streams. struct MyClass { int Int; } obj; cojson::wrapper::istream<std::istream> in(cin); cojson::wrapper::ostream<std::ostream> out(cout); O<MyClass,&MyClass::Int>().read(obj, in); O<MyClass,&MyClass::Int>().write(obj, out); 5.2. With operator<< operator>> cojson::operator namespace provides template operators operator<< / operator>>. These templates require the class to implement either json() method or read(cojson::details::lexer&)/write(cojson::details::ostream&) #include <cojson_stdlib.hpp> using namespace std; using namespace cojson; using namespace cojson::autos; using namespace cojson::operators; struct MyClass { int Int; static inline const auto& json() noexcept { return O<MyClass,&MyClass::Int>(); } } obj; cin >> obj; cout << obj << endl; 5.3. Via a memory buffer cojson::wrapper namespace provides wrappers for memory buffers: buffer and memstream. Both read from and write to a single memory block. memstream maintains two separate position pointers while buffer maintains only one. When no writing is needed, buffer may by supplied with a constant zero-delimited string. #include <cojson_stdlib.hpp> using namespace cojson; using namespace cojson::autos; struct Pdo { int Int; static inline const auto& json() noexcept { return O<Pdo,&Pdo::Int>(); } inline int read(const char* data) noexcept { wrapper::buffer input(data); details::lexer in(input); return json().read(*this, in) ? input.count() : -1; } inline int write(char* data, cojson::size_t size) noexcept { wrapper::buffer output(data, size); return json().write(*this, output) ? output.count() : -1; } }; 5.4. Via a user-defined reader/writer Please refer to COJSON tutorial Section 5.1 and Section 5.2 for details. 6. Additional materials List of COJSON-related posts