This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new ba433e688 feat(c++): add iterator container serialization support
(#3068)
ba433e688 is described below
commit ba433e6889c01eabda1856167d8dca0e9c106a5d
Author: zhan7236 <[email protected]>
AuthorDate: Sun Dec 21 00:43:06 2025 +0800
feat(c++): add iterator container serialization support (#3068)
## Why?
Per xlang specification, Fory needs to support serialization of
iterator-based containers. Currently, only `std::vector` and `std::set`
are supported in C++. This PR adds support for additional sequence
containers (`std::list`, `std::deque`, `std::forward_list`) to provide
more flexibility for C++ users.
## What does this PR do?
Add serialization support for C++ iterator containers:
- `std::list<T>`
- `std::deque<T>`
- `std::forward_list<T>`
All these containers are serialized as `TypeId::LIST` (value 21) per
xlang specification, which only distinguishes between LIST and SET
collection types. Set-like classes (`std::set`, `std::unordered_set`)
continue to use `TypeId::SET`.
**Changes:**
- `serializer_traits.h`: Add `is_list`, `is_deque`, `is_forward_list`
type traits and `is_generic_type` specializations
- `collection_serializer.h`: Add complete `Serializer` specializations
for `std::list`, `std::deque`, `std::forward_list` with full read/write
support
- `type_resolver.h`: Add `FieldTypeBuilder` specializations for new
container types to support struct field serialization
- `collection_serializer_test.cc`: Add 9 comprehensive test cases
covering string, integer, and empty collection scenarios
## Related issues
Closes #2911
## Does this PR introduce any user-facing change?
- [x] Does this PR introduce any public API change?
- Adds new public serialization support for `std::list`, `std::deque`,
and `std::forward_list` containers
- [ ] Does this PR introduce any binary protocol compatibility change?
- No, uses existing `TypeId::LIST` protocol
## Benchmark
N/A - This PR adds new functionality without modifying existing
serialization paths. The new container serializers follow the same
patterns as the existing `std::vector` serializer.
---
cpp/fory/serialization/collection_serializer.h | 802 +++++++++++++++++++++
.../serialization/collection_serializer_test.cc | 251 +++++++
cpp/fory/serialization/serializer_traits.h | 41 ++
cpp/fory/serialization/type_resolver.h | 51 +-
4 files changed, 1138 insertions(+), 7 deletions(-)
diff --git a/cpp/fory/serialization/collection_serializer.h
b/cpp/fory/serialization/collection_serializer.h
index e3d27e3a8..e83ae15da 100644
--- a/cpp/fory/serialization/collection_serializer.h
+++ b/cpp/fory/serialization/collection_serializer.h
@@ -23,7 +23,10 @@
#include "fory/serialization/serializer.h"
#include <array>
#include <cstdint>
+#include <deque>
+#include <forward_list>
#include <limits>
+#include <list>
#include <set>
#include <typeindex>
#include <unordered_set>
@@ -898,6 +901,805 @@ template <typename Alloc> struct
Serializer<std::vector<bool, Alloc>> {
}
};
+// ============================================================================
+// std::list serializer
+// ============================================================================
+
+template <typename T, typename Alloc> struct Serializer<std::list<T, Alloc>> {
+ static constexpr TypeId type_id = TypeId::LIST;
+
+ static inline void write_type_info(WriteContext &ctx) {
+ ctx.write_varuint32(static_cast<uint32_t>(type_id));
+ }
+
+ static inline void read_type_info(ReadContext &ctx) {
+ uint32_t actual = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return;
+ }
+ if (!type_id_matches(actual, static_cast<uint32_t>(type_id))) {
+ ctx.set_error(
+ Error::type_mismatch(actual, static_cast<uint32_t>(type_id)));
+ }
+ }
+
+ static inline std::list<T, Alloc> read(ReadContext &ctx, bool read_ref,
+ bool read_type) {
+ // List-level reference flag
+ bool has_value = consume_ref_flag(ctx, read_ref);
+ if (ctx.has_error() || !has_value) {
+ return std::list<T, Alloc>();
+ }
+
+ // Optional type info for polymorphic containers
+ if (read_type) {
+ uint32_t type_id_read = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::list<T, Alloc>();
+ }
+ uint32_t low = type_id_read & 0xffu;
+ if (low != static_cast<uint32_t>(type_id)) {
+ ctx.set_error(
+ Error::type_mismatch(type_id_read,
static_cast<uint32_t>(type_id)));
+ return std::list<T, Alloc>();
+ }
+ }
+
+ // Length written via writeVarUint32Small7
+ uint32_t length = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::list<T, Alloc>();
+ }
+ // Per xlang spec: header and type_info are omitted when length is 0
+ if (length == 0) {
+ return std::list<T, Alloc>();
+ }
+
+ // Dispatch to slow path for polymorphic/shared-ref elements
+ constexpr bool is_slow_path = is_polymorphic_v<T> || is_shared_ref_v<T>;
+ if constexpr (is_slow_path) {
+ return read_collection_data_slow<T, std::list<T, Alloc>>(ctx, length);
+ } else {
+ // Fast path for non-polymorphic, non-shared-ref elements
+
+ // Elements header bitmap (CollectionFlags)
+ uint8_t bitmap = ctx.read_uint8(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::list<T, Alloc>();
+ }
+ bool track_ref = (bitmap & COLL_TRACKING_REF) != 0;
+ bool has_null = (bitmap & COLL_HAS_NULL) != 0;
+ bool is_decl_type = (bitmap & COLL_DECL_ELEMENT_TYPE) != 0;
+ bool is_same_type = (bitmap & COLL_IS_SAME_TYPE) != 0;
+
+ // Read element type info if IS_SAME_TYPE is set but IS_DECL_ELEMENT_TYPE
+ // is not.
+ if (is_same_type && !is_decl_type) {
+ const TypeInfo *elem_type_info = ctx.read_any_typeinfo(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::list<T, Alloc>();
+ }
+ using ElemType = nullable_element_t<T>;
+ uint32_t expected =
+ static_cast<uint32_t>(Serializer<ElemType>::type_id);
+ if (!type_id_matches(elem_type_info->type_id, expected)) {
+ ctx.set_error(
+ Error::type_mismatch(elem_type_info->type_id, expected));
+ return std::list<T, Alloc>();
+ }
+ }
+
+ std::list<T, Alloc> result;
+
+ // Fast path: no tracking, no nulls, elements have declared type
+ if (!track_ref && !has_null && is_same_type) {
+ for (uint32_t i = 0; i < length; ++i) {
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return result;
+ }
+ auto elem = Serializer<T>::read(ctx, false, false);
+ result.push_back(std::move(elem));
+ }
+ return result;
+ }
+
+ // General path: handle HAS_NULL and/or TRACKING_REF
+ for (uint32_t i = 0; i < length; ++i) {
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return result;
+ }
+ if (track_ref) {
+ auto elem = Serializer<T>::read(ctx, true, false);
+ result.push_back(std::move(elem));
+ } else if (has_null) {
+ bool has_value_elem = consume_ref_flag(ctx, true);
+ if (!has_value_elem) {
+ result.emplace_back();
+ } else {
+ if constexpr (is_nullable_v<T>) {
+ using Inner = nullable_element_t<T>;
+ auto inner = Serializer<Inner>::read(ctx, false, false);
+ result.emplace_back(std::move(inner));
+ } else {
+ auto elem = Serializer<T>::read(ctx, false, false);
+ result.push_back(std::move(elem));
+ }
+ }
+ } else {
+ auto elem = Serializer<T>::read(ctx, false, false);
+ result.push_back(std::move(elem));
+ }
+ }
+
+ return result;
+ }
+ }
+
+ static inline void write(const std::list<T, Alloc> &lst, WriteContext &ctx,
+ bool write_ref, bool write_type,
+ bool has_generics = false) {
+ // Write ref flag if requested
+ write_not_null_ref_flag(ctx, write_ref);
+
+ // Write type info if requested
+ if (write_type) {
+ ctx.write_varuint32(static_cast<uint32_t>(type_id));
+ }
+
+ write_data_generic(lst, ctx, has_generics);
+ }
+
+ static inline void write_data(const std::list<T, Alloc> &lst,
+ WriteContext &ctx) {
+ ctx.write_varuint32(static_cast<uint32_t>(lst.size()));
+ for (const auto &elem : lst) {
+ Serializer<T>::write_data(elem, ctx);
+ }
+ }
+
+ static inline void write_data_generic(const std::list<T, Alloc> &lst,
+ WriteContext &ctx, bool has_generics) {
+ // Dispatch to fast or slow path based on element type characteristics
+ constexpr bool is_fast_path = !is_polymorphic_v<T> && !is_shared_ref_v<T>;
+
+ if constexpr (is_fast_path) {
+ write_collection_data_fast<T>(lst, ctx, has_generics);
+ } else {
+ write_collection_data_slow<T>(lst, ctx, has_generics);
+ }
+ }
+
+ static inline std::list<T, Alloc>
+ read_with_type_info(ReadContext &ctx, bool read_ref,
+ const TypeInfo &type_info) {
+ return read(ctx, read_ref, false);
+ }
+
+ static inline std::list<T, Alloc> read_data(ReadContext &ctx) {
+ uint32_t size = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::list<T, Alloc>();
+ }
+ std::list<T, Alloc> result;
+ for (uint32_t i = 0; i < size; ++i) {
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return result;
+ }
+ auto elem = Serializer<T>::read_data(ctx);
+ result.push_back(std::move(elem));
+ }
+ return result;
+ }
+};
+
+// ============================================================================
+// std::deque serializer
+// ============================================================================
+
+template <typename T, typename Alloc> struct Serializer<std::deque<T, Alloc>> {
+ static constexpr TypeId type_id = TypeId::LIST;
+
+ static inline void write_type_info(WriteContext &ctx) {
+ ctx.write_varuint32(static_cast<uint32_t>(type_id));
+ }
+
+ static inline void read_type_info(ReadContext &ctx) {
+ uint32_t actual = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return;
+ }
+ if (!type_id_matches(actual, static_cast<uint32_t>(type_id))) {
+ ctx.set_error(
+ Error::type_mismatch(actual, static_cast<uint32_t>(type_id)));
+ }
+ }
+
+ static inline std::deque<T, Alloc> read(ReadContext &ctx, bool read_ref,
+ bool read_type) {
+ // Deque-level reference flag
+ bool has_value = consume_ref_flag(ctx, read_ref);
+ if (ctx.has_error() || !has_value) {
+ return std::deque<T, Alloc>();
+ }
+
+ // Optional type info for polymorphic containers
+ if (read_type) {
+ uint32_t type_id_read = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::deque<T, Alloc>();
+ }
+ uint32_t low = type_id_read & 0xffu;
+ if (low != static_cast<uint32_t>(type_id)) {
+ ctx.set_error(
+ Error::type_mismatch(type_id_read,
static_cast<uint32_t>(type_id)));
+ return std::deque<T, Alloc>();
+ }
+ }
+
+ // Length written via writeVarUint32Small7
+ uint32_t length = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::deque<T, Alloc>();
+ }
+ // Per xlang spec: header and type_info are omitted when length is 0
+ if (length == 0) {
+ return std::deque<T, Alloc>();
+ }
+
+ // Dispatch to slow path for polymorphic/shared-ref elements
+ constexpr bool is_slow_path = is_polymorphic_v<T> || is_shared_ref_v<T>;
+ if constexpr (is_slow_path) {
+ return read_collection_data_slow<T, std::deque<T, Alloc>>(ctx, length);
+ } else {
+ // Fast path for non-polymorphic, non-shared-ref elements
+
+ // Elements header bitmap (CollectionFlags)
+ uint8_t bitmap = ctx.read_uint8(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::deque<T, Alloc>();
+ }
+ bool track_ref = (bitmap & COLL_TRACKING_REF) != 0;
+ bool has_null = (bitmap & COLL_HAS_NULL) != 0;
+ bool is_decl_type = (bitmap & COLL_DECL_ELEMENT_TYPE) != 0;
+ bool is_same_type = (bitmap & COLL_IS_SAME_TYPE) != 0;
+
+ // Read element type info if IS_SAME_TYPE is set but IS_DECL_ELEMENT_TYPE
+ // is not.
+ if (is_same_type && !is_decl_type) {
+ const TypeInfo *elem_type_info = ctx.read_any_typeinfo(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::deque<T, Alloc>();
+ }
+ using ElemType = nullable_element_t<T>;
+ uint32_t expected =
+ static_cast<uint32_t>(Serializer<ElemType>::type_id);
+ if (!type_id_matches(elem_type_info->type_id, expected)) {
+ ctx.set_error(
+ Error::type_mismatch(elem_type_info->type_id, expected));
+ return std::deque<T, Alloc>();
+ }
+ }
+
+ std::deque<T, Alloc> result;
+
+ // Fast path: no tracking, no nulls, elements have declared type
+ if (!track_ref && !has_null && is_same_type) {
+ for (uint32_t i = 0; i < length; ++i) {
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return result;
+ }
+ auto elem = Serializer<T>::read(ctx, false, false);
+ result.push_back(std::move(elem));
+ }
+ return result;
+ }
+
+ // General path: handle HAS_NULL and/or TRACKING_REF
+ for (uint32_t i = 0; i < length; ++i) {
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return result;
+ }
+ if (track_ref) {
+ auto elem = Serializer<T>::read(ctx, true, false);
+ result.push_back(std::move(elem));
+ } else if (has_null) {
+ bool has_value_elem = consume_ref_flag(ctx, true);
+ if (!has_value_elem) {
+ result.emplace_back();
+ } else {
+ if constexpr (is_nullable_v<T>) {
+ using Inner = nullable_element_t<T>;
+ auto inner = Serializer<Inner>::read(ctx, false, false);
+ result.emplace_back(std::move(inner));
+ } else {
+ auto elem = Serializer<T>::read(ctx, false, false);
+ result.push_back(std::move(elem));
+ }
+ }
+ } else {
+ auto elem = Serializer<T>::read(ctx, false, false);
+ result.push_back(std::move(elem));
+ }
+ }
+
+ return result;
+ }
+ }
+
+ static inline void write(const std::deque<T, Alloc> &deq, WriteContext &ctx,
+ bool write_ref, bool write_type,
+ bool has_generics = false) {
+ // Write ref flag if requested
+ write_not_null_ref_flag(ctx, write_ref);
+
+ // Write type info if requested
+ if (write_type) {
+ ctx.write_varuint32(static_cast<uint32_t>(type_id));
+ }
+
+ write_data_generic(deq, ctx, has_generics);
+ }
+
+ static inline void write_data(const std::deque<T, Alloc> &deq,
+ WriteContext &ctx) {
+ ctx.write_varuint32(static_cast<uint32_t>(deq.size()));
+ for (const auto &elem : deq) {
+ Serializer<T>::write_data(elem, ctx);
+ }
+ }
+
+ static inline void write_data_generic(const std::deque<T, Alloc> &deq,
+ WriteContext &ctx, bool has_generics) {
+ // Dispatch to fast or slow path based on element type characteristics
+ constexpr bool is_fast_path = !is_polymorphic_v<T> && !is_shared_ref_v<T>;
+
+ if constexpr (is_fast_path) {
+ write_collection_data_fast<T>(deq, ctx, has_generics);
+ } else {
+ write_collection_data_slow<T>(deq, ctx, has_generics);
+ }
+ }
+
+ static inline std::deque<T, Alloc>
+ read_with_type_info(ReadContext &ctx, bool read_ref,
+ const TypeInfo &type_info) {
+ return read(ctx, read_ref, false);
+ }
+
+ static inline std::deque<T, Alloc> read_data(ReadContext &ctx) {
+ uint32_t size = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::deque<T, Alloc>();
+ }
+ std::deque<T, Alloc> result;
+ for (uint32_t i = 0; i < size; ++i) {
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return result;
+ }
+ auto elem = Serializer<T>::read_data(ctx);
+ result.push_back(std::move(elem));
+ }
+ return result;
+ }
+};
+
+// ============================================================================
+// std::forward_list serializer
+// ============================================================================
+
+template <typename T, typename Alloc>
+struct Serializer<std::forward_list<T, Alloc>> {
+ static constexpr TypeId type_id = TypeId::LIST;
+
+ static inline void write_type_info(WriteContext &ctx) {
+ ctx.write_varuint32(static_cast<uint32_t>(type_id));
+ }
+
+ static inline void read_type_info(ReadContext &ctx) {
+ uint32_t actual = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return;
+ }
+ if (!type_id_matches(actual, static_cast<uint32_t>(type_id))) {
+ ctx.set_error(
+ Error::type_mismatch(actual, static_cast<uint32_t>(type_id)));
+ }
+ }
+
+ static inline std::forward_list<T, Alloc>
+ read(ReadContext &ctx, bool read_ref, bool read_type) {
+ // List-level reference flag
+ bool has_value = consume_ref_flag(ctx, read_ref);
+ if (ctx.has_error() || !has_value) {
+ return std::forward_list<T, Alloc>();
+ }
+
+ // Optional type info for polymorphic containers
+ if (read_type) {
+ uint32_t type_id_read = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::forward_list<T, Alloc>();
+ }
+ uint32_t low = type_id_read & 0xffu;
+ if (low != static_cast<uint32_t>(type_id)) {
+ ctx.set_error(
+ Error::type_mismatch(type_id_read,
static_cast<uint32_t>(type_id)));
+ return std::forward_list<T, Alloc>();
+ }
+ }
+
+ // Length written via writeVarUint32Small7
+ uint32_t length = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::forward_list<T, Alloc>();
+ }
+ // Per xlang spec: header and type_info are omitted when length is 0
+ if (length == 0) {
+ return std::forward_list<T, Alloc>();
+ }
+
+ // Read elements into a temporary vector then build forward_list
+ // (forward_list doesn't have push_back, only push_front)
+ std::vector<T> temp;
+ temp.reserve(length);
+
+ // Dispatch to slow path for polymorphic/shared-ref elements
+ constexpr bool is_slow_path = is_polymorphic_v<T> || is_shared_ref_v<T>;
+ if constexpr (is_slow_path) {
+ temp = read_collection_data_slow<T, std::vector<T>>(ctx, length);
+ } else {
+ // Fast path for non-polymorphic, non-shared-ref elements
+
+ // Elements header bitmap (CollectionFlags)
+ uint8_t bitmap = ctx.read_uint8(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::forward_list<T, Alloc>();
+ }
+ bool track_ref = (bitmap & COLL_TRACKING_REF) != 0;
+ bool has_null = (bitmap & COLL_HAS_NULL) != 0;
+ bool is_decl_type = (bitmap & COLL_DECL_ELEMENT_TYPE) != 0;
+ bool is_same_type = (bitmap & COLL_IS_SAME_TYPE) != 0;
+
+ // Read element type info if IS_SAME_TYPE is set but IS_DECL_ELEMENT_TYPE
+ // is not.
+ if (is_same_type && !is_decl_type) {
+ const TypeInfo *elem_type_info = ctx.read_any_typeinfo(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::forward_list<T, Alloc>();
+ }
+ using ElemType = nullable_element_t<T>;
+ uint32_t expected =
+ static_cast<uint32_t>(Serializer<ElemType>::type_id);
+ if (!type_id_matches(elem_type_info->type_id, expected)) {
+ ctx.set_error(
+ Error::type_mismatch(elem_type_info->type_id, expected));
+ return std::forward_list<T, Alloc>();
+ }
+ }
+
+ // Fast path: no tracking, no nulls, elements have declared type
+ if (!track_ref && !has_null && is_same_type) {
+ for (uint32_t i = 0; i < length; ++i) {
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ break;
+ }
+ auto elem = Serializer<T>::read(ctx, false, false);
+ temp.push_back(std::move(elem));
+ }
+ } else {
+ // General path: handle HAS_NULL and/or TRACKING_REF
+ for (uint32_t i = 0; i < length; ++i) {
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ break;
+ }
+ if (track_ref) {
+ auto elem = Serializer<T>::read(ctx, true, false);
+ temp.push_back(std::move(elem));
+ } else if (has_null) {
+ bool has_value_elem = consume_ref_flag(ctx, true);
+ if (!has_value_elem) {
+ temp.emplace_back();
+ } else {
+ if constexpr (is_nullable_v<T>) {
+ using Inner = nullable_element_t<T>;
+ auto inner = Serializer<Inner>::read(ctx, false, false);
+ temp.emplace_back(std::move(inner));
+ } else {
+ auto elem = Serializer<T>::read(ctx, false, false);
+ temp.push_back(std::move(elem));
+ }
+ }
+ } else {
+ auto elem = Serializer<T>::read(ctx, false, false);
+ temp.push_back(std::move(elem));
+ }
+ }
+ }
+ }
+
+ // Build forward_list in reverse order using push_front
+ std::forward_list<T, Alloc> result;
+ for (auto it = temp.rbegin(); it != temp.rend(); ++it) {
+ result.push_front(std::move(*it));
+ }
+ return result;
+ }
+
+ static inline void write(const std::forward_list<T, Alloc> &lst,
+ WriteContext &ctx, bool write_ref, bool write_type,
+ bool has_generics = false) {
+ // Write ref flag if requested
+ write_not_null_ref_flag(ctx, write_ref);
+
+ // Write type info if requested
+ if (write_type) {
+ ctx.write_varuint32(static_cast<uint32_t>(type_id));
+ }
+
+ write_data_generic(lst, ctx, has_generics);
+ }
+
+ static inline void write_data(const std::forward_list<T, Alloc> &lst,
+ WriteContext &ctx) {
+ // forward_list doesn't have size(), so we need to count elements first
+ uint32_t size = 0;
+ for (const auto &elem : lst) {
+ (void)elem;
+ ++size;
+ }
+ ctx.write_varuint32(size);
+ for (const auto &elem : lst) {
+ Serializer<T>::write_data(elem, ctx);
+ }
+ }
+
+ static inline void write_data_generic(const std::forward_list<T, Alloc> &lst,
+ WriteContext &ctx, bool has_generics) {
+ // Convert to vector first for efficient writing (forward_list has no size)
+ std::vector<std::reference_wrapper<const T>> temp;
+ for (const auto &elem : lst) {
+ temp.push_back(std::cref(elem));
+ }
+
+ // Write length
+ ctx.write_varuint32(static_cast<uint32_t>(temp.size()));
+
+ if (temp.empty()) {
+ return;
+ }
+
+ // Dispatch to fast or slow path based on element type characteristics
+ constexpr bool is_fast_path = !is_polymorphic_v<T> && !is_shared_ref_v<T>;
+
+ if constexpr (is_fast_path) {
+ // Check for null elements
+ bool has_null = false;
+ if constexpr (is_nullable_v<T>) {
+ for (const auto &elem_ref : temp) {
+ if (is_null_value(elem_ref.get())) {
+ has_null = true;
+ break;
+ }
+ }
+ }
+
+ // Build header bitmap
+ uint8_t bitmap = COLL_IS_SAME_TYPE;
+ if (has_null) {
+ bitmap |= COLL_HAS_NULL;
+ }
+
+ // Determine if element type is declared
+ using ElemType = nullable_element_t<T>;
+ bool is_elem_declared =
+ has_generics && !need_type_for_collection_elem<ElemType>();
+ if (is_elem_declared) {
+ bitmap |= COLL_DECL_ELEMENT_TYPE;
+ }
+
+ // Write header
+ ctx.write_uint8(bitmap);
+
+ // Write element type info if not declared
+ if (!is_elem_declared) {
+ Serializer<ElemType>::write_type_info(ctx);
+ }
+
+ // Write elements
+ if constexpr (is_nullable_v<T>) {
+ using Inner = nullable_element_t<T>;
+ if (has_null) {
+ for (const auto &elem_ref : temp) {
+ const auto &elem = elem_ref.get();
+ if (is_null_value(elem)) {
+ ctx.write_int8(NULL_FLAG);
+ } else {
+ ctx.write_int8(NOT_NULL_VALUE_FLAG);
+ if (is_elem_declared) {
+ Serializer<Inner>::write_data(deref_nullable(elem), ctx);
+ } else {
+ Serializer<Inner>::write(deref_nullable(elem), ctx, false,
+ false);
+ }
+ }
+ }
+ } else {
+ for (const auto &elem_ref : temp) {
+ const auto &elem = elem_ref.get();
+ if (is_elem_declared) {
+ Serializer<Inner>::write_data(deref_nullable(elem), ctx);
+ } else {
+ Serializer<Inner>::write(deref_nullable(elem), ctx, false,
false);
+ }
+ }
+ }
+ } else {
+ for (const auto &elem_ref : temp) {
+ const auto &elem = elem_ref.get();
+ if (is_elem_declared) {
+ if constexpr (is_generic_type_v<T>) {
+ Serializer<T>::write_data_generic(elem, ctx, true);
+ } else {
+ Serializer<T>::write_data(elem, ctx);
+ }
+ } else {
+ Serializer<T>::write(elem, ctx, false, false);
+ }
+ }
+ }
+ } else {
+ // Slow path for polymorphic or shared-ref elements
+ constexpr bool elem_is_polymorphic = is_polymorphic_v<T>;
+ constexpr bool elem_is_shared_ref = is_shared_ref_v<T>;
+
+ using ElemType = nullable_element_t<T>;
+ bool is_elem_declared =
+ has_generics && !need_type_for_collection_elem<ElemType>();
+
+ // Scan collection to determine header flags
+ bool has_null = false;
+ bool is_same_type = true;
+ std::type_index first_type{typeid(void)};
+ bool first_type_set = false;
+
+ for (const auto &elem_ref : temp) {
+ const auto &elem = elem_ref.get();
+ // Check for nulls
+ if constexpr (is_nullable_v<T>) {
+ if (is_null_value(elem)) {
+ has_null = true;
+ continue;
+ }
+ }
+ // Check runtime types for polymorphic elements
+ if constexpr (elem_is_polymorphic) {
+ if (is_same_type) {
+ auto concrete_id = get_concrete_type_id(elem);
+ if (!first_type_set) {
+ first_type = concrete_id;
+ first_type_set = true;
+ } else if (concrete_id != first_type) {
+ is_same_type = false;
+ }
+ }
+ }
+ }
+
+ // If all polymorphic elements are null, treat as heterogeneous
+ if constexpr (elem_is_polymorphic) {
+ if (is_same_type && !first_type_set) {
+ is_same_type = false;
+ }
+ }
+
+ // Build header bitmap
+ uint8_t bitmap = 0;
+ if (has_null) {
+ bitmap |= COLL_HAS_NULL;
+ }
+ if (is_elem_declared && !elem_is_polymorphic) {
+ bitmap |= COLL_DECL_ELEMENT_TYPE;
+ }
+ if (is_same_type) {
+ bitmap |= COLL_IS_SAME_TYPE;
+ }
+ if constexpr (elem_is_shared_ref) {
+ bitmap |= COLL_TRACKING_REF;
+ }
+
+ // Write header
+ ctx.write_uint8(bitmap);
+
+ // Write element type info if IS_SAME_TYPE && !IS_DECL_ELEMENT_TYPE
+ if (is_same_type && !(bitmap & COLL_DECL_ELEMENT_TYPE)) {
+ if constexpr (elem_is_polymorphic) {
+ ctx.write_any_typeinfo(static_cast<uint32_t>(TypeId::UNKNOWN),
+ first_type);
+ } else {
+ Serializer<ElemType>::write_type_info(ctx);
+ }
+ }
+
+ // Write elements
+ if (is_same_type) {
+ if (!has_null) {
+ if constexpr (elem_is_shared_ref) {
+ for (const auto &elem_ref : temp) {
+ Serializer<T>::write(elem_ref.get(), ctx, true, false,
+ has_generics);
+ }
+ } else {
+ for (const auto &elem_ref : temp) {
+ const auto &elem = elem_ref.get();
+ if constexpr (is_nullable_v<T>) {
+ using Inner = nullable_element_t<T>;
+ Serializer<Inner>::write_data(deref_nullable(elem), ctx);
+ } else {
+ if constexpr (is_generic_type_v<T>) {
+ Serializer<T>::write_data_generic(elem, ctx, has_generics);
+ } else {
+ Serializer<T>::write_data(elem, ctx);
+ }
+ }
+ }
+ }
+ } else {
+ for (const auto &elem_ref : temp) {
+ Serializer<T>::write(elem_ref.get(), ctx, true, false,
+ has_generics);
+ }
+ }
+ } else {
+ if (!has_null) {
+ if constexpr (elem_is_shared_ref) {
+ for (const auto &elem_ref : temp) {
+ Serializer<T>::write(elem_ref.get(), ctx, true, true,
+ has_generics);
+ }
+ } else {
+ for (const auto &elem_ref : temp) {
+ Serializer<T>::write(elem_ref.get(), ctx, false, true,
+ has_generics);
+ }
+ }
+ } else {
+ for (const auto &elem_ref : temp) {
+ Serializer<T>::write(elem_ref.get(), ctx, true, true,
has_generics);
+ }
+ }
+ }
+ }
+ }
+
+ static inline std::forward_list<T, Alloc>
+ read_with_type_info(ReadContext &ctx, bool read_ref,
+ const TypeInfo &type_info) {
+ return read(ctx, read_ref, false);
+ }
+
+ static inline std::forward_list<T, Alloc> read_data(ReadContext &ctx) {
+ uint32_t size = ctx.read_varuint32(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::forward_list<T, Alloc>();
+ }
+ std::vector<T> temp;
+ temp.reserve(size);
+ for (uint32_t i = 0; i < size; ++i) {
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ break;
+ }
+ auto elem = Serializer<T>::read_data(ctx);
+ temp.push_back(std::move(elem));
+ }
+ // Build forward_list in reverse order
+ std::forward_list<T, Alloc> result;
+ for (auto it = temp.rbegin(); it != temp.rend(); ++it) {
+ result.push_front(std::move(*it));
+ }
+ return result;
+ }
+};
+
// ============================================================================
// std::set serializer
// ============================================================================
diff --git a/cpp/fory/serialization/collection_serializer_test.cc
b/cpp/fory/serialization/collection_serializer_test.cc
index 7e70c0758..e8dddf5f4 100644
--- a/cpp/fory/serialization/collection_serializer_test.cc
+++ b/cpp/fory/serialization/collection_serializer_test.cc
@@ -20,6 +20,9 @@
#include "fory/serialization/fory.h"
#include "gtest/gtest.h"
#include <cstdint>
+#include <deque>
+#include <forward_list>
+#include <list>
#include <memory>
#include <optional>
#include <string>
@@ -368,6 +371,254 @@ TEST(CollectionSerializerTest, VectorOptionalWithNulls) {
EXPECT_EQ(*deserialized.values[2], "third");
}
+// ============================================================================
+// std::list Tests
+// ============================================================================
+
+struct ListStringHolder {
+ std::list<std::string> strings;
+};
+FORY_STRUCT(ListStringHolder, strings);
+
+struct ListIntHolder {
+ std::list<int32_t> numbers;
+};
+FORY_STRUCT(ListIntHolder, numbers);
+
+TEST(CollectionSerializerTest, ListStringRoundTrip) {
+ auto fory = Fory::builder().xlang(true).build();
+ fory.register_struct<ListStringHolder>(300);
+
+ ListStringHolder original;
+ original.strings = {"hello", "world", "fory", "list"};
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<ListStringHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_EQ(deserialized.strings.size(), 4u);
+ auto it = deserialized.strings.begin();
+ EXPECT_EQ(*it++, "hello");
+ EXPECT_EQ(*it++, "world");
+ EXPECT_EQ(*it++, "fory");
+ EXPECT_EQ(*it++, "list");
+}
+
+TEST(CollectionSerializerTest, ListIntRoundTrip) {
+ auto fory = Fory::builder().xlang(true).build();
+ fory.register_struct<ListIntHolder>(301);
+
+ ListIntHolder original;
+ original.numbers = {1, 2, 3, 42, 100};
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<ListIntHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_EQ(deserialized.numbers.size(), 5u);
+ auto it = deserialized.numbers.begin();
+ EXPECT_EQ(*it++, 1);
+ EXPECT_EQ(*it++, 2);
+ EXPECT_EQ(*it++, 3);
+ EXPECT_EQ(*it++, 42);
+ EXPECT_EQ(*it++, 100);
+}
+
+TEST(CollectionSerializerTest, ListEmptyRoundTrip) {
+ auto fory = Fory::builder().xlang(true).build();
+ fory.register_struct<ListStringHolder>(302);
+
+ ListStringHolder original;
+ // Empty list
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<ListStringHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ EXPECT_TRUE(deserialized.strings.empty());
+}
+
+// ============================================================================
+// std::deque Tests
+// ============================================================================
+
+struct DequeStringHolder {
+ std::deque<std::string> strings;
+};
+FORY_STRUCT(DequeStringHolder, strings);
+
+struct DequeIntHolder {
+ std::deque<int32_t> numbers;
+};
+FORY_STRUCT(DequeIntHolder, numbers);
+
+TEST(CollectionSerializerTest, DequeStringRoundTrip) {
+ auto fory = Fory::builder().xlang(true).build();
+ fory.register_struct<DequeStringHolder>(400);
+
+ DequeStringHolder original;
+ original.strings = {"hello", "world", "fory", "deque"};
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<DequeStringHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_EQ(deserialized.strings.size(), 4u);
+ EXPECT_EQ(deserialized.strings[0], "hello");
+ EXPECT_EQ(deserialized.strings[1], "world");
+ EXPECT_EQ(deserialized.strings[2], "fory");
+ EXPECT_EQ(deserialized.strings[3], "deque");
+}
+
+TEST(CollectionSerializerTest, DequeIntRoundTrip) {
+ auto fory = Fory::builder().xlang(true).build();
+ fory.register_struct<DequeIntHolder>(401);
+
+ DequeIntHolder original;
+ original.numbers = {10, 20, 30, 40, 50};
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<DequeIntHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ ASSERT_EQ(deserialized.numbers.size(), 5u);
+ EXPECT_EQ(deserialized.numbers[0], 10);
+ EXPECT_EQ(deserialized.numbers[1], 20);
+ EXPECT_EQ(deserialized.numbers[2], 30);
+ EXPECT_EQ(deserialized.numbers[3], 40);
+ EXPECT_EQ(deserialized.numbers[4], 50);
+}
+
+TEST(CollectionSerializerTest, DequeEmptyRoundTrip) {
+ auto fory = Fory::builder().xlang(true).build();
+ fory.register_struct<DequeStringHolder>(402);
+
+ DequeStringHolder original;
+ // Empty deque
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<DequeStringHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ EXPECT_TRUE(deserialized.strings.empty());
+}
+
+// ============================================================================
+// std::forward_list Tests
+// ============================================================================
+
+struct ForwardListStringHolder {
+ std::forward_list<std::string> strings;
+};
+FORY_STRUCT(ForwardListStringHolder, strings);
+
+struct ForwardListIntHolder {
+ std::forward_list<int32_t> numbers;
+};
+FORY_STRUCT(ForwardListIntHolder, numbers);
+
+TEST(CollectionSerializerTest, ForwardListStringRoundTrip) {
+ auto fory = Fory::builder().xlang(true).build();
+ fory.register_struct<ForwardListStringHolder>(500);
+
+ ForwardListStringHolder original;
+ original.strings = {"hello", "world", "fory", "forward_list"};
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<ForwardListStringHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ // Convert to vector for easier comparison
+ std::vector<std::string> result(deserialized.strings.begin(),
+ deserialized.strings.end());
+ ASSERT_EQ(result.size(), 4u);
+ EXPECT_EQ(result[0], "hello");
+ EXPECT_EQ(result[1], "world");
+ EXPECT_EQ(result[2], "fory");
+ EXPECT_EQ(result[3], "forward_list");
+}
+
+TEST(CollectionSerializerTest, ForwardListIntRoundTrip) {
+ auto fory = Fory::builder().xlang(true).build();
+ fory.register_struct<ForwardListIntHolder>(501);
+
+ ForwardListIntHolder original;
+ original.numbers = {100, 200, 300, 400, 500};
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<ForwardListIntHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ // Convert to vector for easier comparison
+ std::vector<int32_t> result(deserialized.numbers.begin(),
+ deserialized.numbers.end());
+ ASSERT_EQ(result.size(), 5u);
+ EXPECT_EQ(result[0], 100);
+ EXPECT_EQ(result[1], 200);
+ EXPECT_EQ(result[2], 300);
+ EXPECT_EQ(result[3], 400);
+ EXPECT_EQ(result[4], 500);
+}
+
+TEST(CollectionSerializerTest, ForwardListEmptyRoundTrip) {
+ auto fory = Fory::builder().xlang(true).build();
+ fory.register_struct<ForwardListStringHolder>(502);
+
+ ForwardListStringHolder original;
+ // Empty forward_list
+
+ auto bytes_result = fory.serialize(original);
+ ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+ auto deserialize_result = fory.deserialize<ForwardListStringHolder>(
+ bytes_result->data(), bytes_result->size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << deserialize_result.error().to_string();
+
+ auto deserialized = std::move(deserialize_result).value();
+ EXPECT_TRUE(deserialized.strings.empty());
+}
+
} // namespace
} // namespace serialization
} // namespace fory
diff --git a/cpp/fory/serialization/serializer_traits.h
b/cpp/fory/serialization/serializer_traits.h
index b1846b9fe..7f3ad3d71 100644
--- a/cpp/fory/serialization/serializer_traits.h
+++ b/cpp/fory/serialization/serializer_traits.h
@@ -25,6 +25,7 @@
#include "fory/type/type.h"
#include <array>
#include <deque>
+#include <forward_list>
#include <list>
#include <map>
#include <memory>
@@ -78,6 +79,31 @@ struct is_vector<std::vector<T, Alloc>> : std::true_type {};
template <typename T> inline constexpr bool is_vector_v = is_vector<T>::value;
+/// Detect std::list
+template <typename T> struct is_list : std::false_type {};
+
+template <typename T, typename Alloc>
+struct is_list<std::list<T, Alloc>> : std::true_type {};
+
+template <typename T> inline constexpr bool is_list_v = is_list<T>::value;
+
+/// Detect std::deque
+template <typename T> struct is_deque : std::false_type {};
+
+template <typename T, typename Alloc>
+struct is_deque<std::deque<T, Alloc>> : std::true_type {};
+
+template <typename T> inline constexpr bool is_deque_v = is_deque<T>::value;
+
+/// Detect std::forward_list
+template <typename T> struct is_forward_list : std::false_type {};
+
+template <typename T, typename Alloc>
+struct is_forward_list<std::forward_list<T, Alloc>> : std::true_type {};
+
+template <typename T>
+inline constexpr bool is_forward_list_v = is_forward_list<T>::value;
+
/// Detect std::optional
template <typename T> struct is_optional : std::false_type {};
@@ -241,6 +267,15 @@ struct is_generic_type<std::set<T, Args...>> :
std::true_type {};
template <typename T, typename... Args>
struct is_generic_type<std::unordered_set<T, Args...>> : std::true_type {};
+template <typename T, typename Alloc>
+struct is_generic_type<std::list<T, Alloc>> : std::true_type {};
+
+template <typename T, typename Alloc>
+struct is_generic_type<std::deque<T, Alloc>> : std::true_type {};
+
+template <typename T, typename Alloc>
+struct is_generic_type<std::forward_list<T, Alloc>> : std::true_type {};
+
template <typename... Ts>
struct is_generic_type<std::tuple<Ts...>> : std::true_type {};
@@ -456,6 +491,12 @@ template <typename T> struct TypeIndex<std::deque<T>> {
fnv1a_64_combine(fnv1a_64("std::deque"), type_index<T>());
};
+// forward_list<T>
+template <typename T> struct TypeIndex<std::forward_list<T>> {
+ static constexpr uint64_t value =
+ fnv1a_64_combine(fnv1a_64("std::forward_list"), type_index<T>());
+};
+
// set<T>
template <typename T> struct TypeIndex<std::set<T>> {
static constexpr uint64_t value =
diff --git a/cpp/fory/serialization/type_resolver.h
b/cpp/fory/serialization/type_resolver.h
index 987e70a5e..a4a605735 100644
--- a/cpp/fory/serialization/type_resolver.h
+++ b/cpp/fory/serialization/type_resolver.h
@@ -334,6 +334,42 @@ struct FieldTypeBuilder<T,
std::enable_if_t<is_vector_v<decay_t<T>>>> {
}
};
+template <typename T>
+struct FieldTypeBuilder<T, std::enable_if_t<is_list_v<decay_t<T>>>> {
+ using List = decay_t<T>;
+ using Element = typename List::value_type;
+ static FieldType build(bool nullable) {
+ FieldType elem = FieldTypeBuilder<Element>::build(false);
+ FieldType ft(to_type_id(TypeId::LIST), nullable);
+ ft.generics.push_back(std::move(elem));
+ return ft;
+ }
+};
+
+template <typename T>
+struct FieldTypeBuilder<T, std::enable_if_t<is_deque_v<decay_t<T>>>> {
+ using Deque = decay_t<T>;
+ using Element = typename Deque::value_type;
+ static FieldType build(bool nullable) {
+ FieldType elem = FieldTypeBuilder<Element>::build(false);
+ FieldType ft(to_type_id(TypeId::LIST), nullable);
+ ft.generics.push_back(std::move(elem));
+ return ft;
+ }
+};
+
+template <typename T>
+struct FieldTypeBuilder<T, std::enable_if_t<is_forward_list_v<decay_t<T>>>> {
+ using FList = decay_t<T>;
+ using Element = typename FList::value_type;
+ static FieldType build(bool nullable) {
+ FieldType elem = FieldTypeBuilder<Element>::build(false);
+ FieldType ft(to_type_id(TypeId::LIST), nullable);
+ ft.generics.push_back(std::move(elem));
+ return ft;
+ }
+};
+
template <typename T>
struct FieldTypeBuilder<T, std::enable_if_t<is_set_like_v<decay_t<T>>>> {
using Set = decay_t<T>;
@@ -409,13 +445,14 @@ struct FieldTypeBuilder<T,
std::enable_if_t<std::is_enum_v<decay_t<T>>>> {
template <typename T>
struct FieldTypeBuilder<
- T,
- std::enable_if_t<
- !is_optional_v<decay_t<T>> && !is_shared_ptr_v<decay_t<T>> &&
- !is_unique_ptr_v<decay_t<T>> && !is_vector_v<decay_t<T>> &&
- !is_set_like_v<decay_t<T>> && !is_map_like_v<decay_t<T>> &&
- !is_string_view_v<decay_t<T>> && !is_tuple_v<decay_t<T>> &&
- !std::is_enum_v<decay_t<T>> && has_serializer_type_id_v<decay_t<T>>>> {
+ T, std::enable_if_t<
+ !is_optional_v<decay_t<T>> && !is_shared_ptr_v<decay_t<T>> &&
+ !is_unique_ptr_v<decay_t<T>> && !is_vector_v<decay_t<T>> &&
+ !is_list_v<decay_t<T>> && !is_deque_v<decay_t<T>> &&
+ !is_forward_list_v<decay_t<T>> && !is_set_like_v<decay_t<T>> &&
+ !is_map_like_v<decay_t<T>> && !is_string_view_v<decay_t<T>> &&
+ !is_tuple_v<decay_t<T>> && !std::is_enum_v<decay_t<T>> &&
+ has_serializer_type_id_v<decay_t<T>>>> {
using Decayed = decay_t<T>;
static FieldType build(bool nullable) {
return FieldType(to_type_id(Serializer<Decayed>::type_id), nullable);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]