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 0e4ac5917 feat(cpp): support struct property accessors (#3751)
0e4ac5917 is described below
commit 0e4ac5917898e4fbd253a5d638a21ff15d500d67
Author: Shawn Yang <[email protected]>
AuthorDate: Fri Jun 12 16:36:21 2026 +0800
feat(cpp): support struct property accessors (#3751)
## Why?
C++ struct serialization only accepted data-member field entries, which
made PIMPL-style or private-storage types awkward to serialize when
their stable public surface is an accessor pair.
## What does this PR do?
- Adds `FORY_PROPERTY` field entries for accessor-backed C++ struct
fields, including same-name getter/setter, custom getter/setter, and
optional `fory::F(...)` metadata forms.
- Introduces compile-time property descriptors plus shared field
type/get/set helpers so struct serialization, compatible struct reads,
row encoding, and type metadata can handle both member pointers and
accessor properties.
- Keeps direct-member primitive fast paths limited to direct fields
while routing accessor-backed fields through their setter path.
- Adds C++ coverage for property metadata, PIMPL property round trips,
configured accessor metadata, and compatible nested named structs.
- Documents accessor properties in the C++ serialization and schema
metadata guides.
## Related issues
Closes #3747
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence of the final clean AI review results
from both fresh reviewers on the current PR diff or current HEAD after
the latest code changes.
## Does this PR introduce any user-facing change?
Adds the C++ `FORY_PROPERTY` macro for accessor-backed struct fields.
This does not change the binary protocol.
- [x] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
Not run; this PR changes C++ struct metadata/accessor support and does
not include benchmark changes.
---
cpp/fory/encoder/row_encode_trait.h | 13 +-
cpp/fory/meta/field.h | 36 ++++
cpp/fory/meta/field_info.h | 114 ++++++++--
cpp/fory/meta/field_info_test.cc | 49 +++++
cpp/fory/meta/preprocessor.h | 6 +
cpp/fory/serialization/struct_compatible_test.cc | 71 +++++++
cpp/fory/serialization/struct_serializer.h | 260 +++++++++--------------
cpp/fory/serialization/struct_test.cc | 79 +++++++
cpp/fory/serialization/type_resolver.h | 10 +-
docs/benchmarks/cpp/README.md | 52 ++---
docs/benchmarks/cpp/throughput.png | Bin 103838 -> 102327 bytes
docs/guide/cpp/basic-serialization.md | 61 +++++-
docs/guide/cpp/schema-metadata.md | 17 +-
13 files changed, 551 insertions(+), 217 deletions(-)
diff --git a/cpp/fory/encoder/row_encode_trait.h
b/cpp/fory/encoder/row_encode_trait.h
index ebe034f15..1b039412e 100644
--- a/cpp/fory/encoder/row_encode_trait.h
+++ b/cpp/fory/encoder/row_encode_trait.h
@@ -19,6 +19,7 @@
#pragma once
+#include "fory/meta/field.h"
#include "fory/meta/field_info.h"
#include "fory/meta/type_traits.h"
#include "fory/row/writer.h"
@@ -191,8 +192,8 @@ private:
using FieldInfo = decltype(fory_field_info(std::declval<T>()));
template <size_t I> static FieldPtr get_field() {
- using FieldType = meta::RemoveMemberPointerCVRefT<
- std::tuple_element_t<I, decltype(FieldInfo::ptrs())>>;
+ using FieldEntry = std::tuple_element_t<I, decltype(FieldInfo::ptrs())>;
+ using FieldType = field_value_type_t<T, FieldEntry>;
return field(details::string_view_to_string(FieldInfo::Names[I]),
RowEncodeTrait<FieldType>::Type());
}
@@ -204,10 +205,10 @@ private:
template <size_t I, typename V>
static void write_field(V &&visitor, const T &value, RowWriter &writer) {
- using FieldType = meta::RemoveMemberPointerCVRefT<
- std::tuple_element_t<I, decltype(FieldInfo::ptrs())>>;
- RowEncodeTrait<FieldType>::write(std::forward<V>(visitor),
- value.*std::get<I>(FieldInfo::ptrs_ref()),
+ const auto field_entry = std::get<I>(FieldInfo::ptrs_ref());
+ using FieldType = field_value_type_t<T, decltype(field_entry)>;
+ auto &&field_value = field_value_get(value, field_entry);
+ RowEncodeTrait<FieldType>::write(std::forward<V>(visitor), field_value,
writer, I);
}
diff --git a/cpp/fory/meta/field.h b/cpp/fory/meta/field.h
index faad1c2e7..e66934dc7 100644
--- a/cpp/fory/meta/field.h
+++ b/cpp/fory/meta/field.h
@@ -21,12 +21,14 @@
#include "fory/meta/field_info.h"
#include "fory/type/type.h"
+#include "fory/util/macros.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <string_view>
#include <tuple>
#include <type_traits>
+#include <utility>
namespace fory {
@@ -198,4 +200,38 @@ template <typename T> struct field_dynamic_value {
template <typename T>
inline constexpr int field_dynamic_value_v = field_dynamic_value<T>::value;
+template <typename StructT, typename Entry>
+using field_value_type_t = unwrap_field_t<meta::FieldRawTypeT<
+ std::remove_cv_t<std::remove_reference_t<StructT>>, Entry>>;
+
+template <typename StructT, typename Entry>
+FORY_ALWAYS_INLINE decltype(auto) field_value_get(StructT &&obj, Entry entry) {
+ using RawFieldType =
+ meta::FieldRawTypeT<std::remove_cv_t<std::remove_reference_t<StructT>>,
+ Entry>;
+ if constexpr (meta::IsPropertyDescriptorV<Entry>) {
+ return meta::RemoveCVRefT<Entry>::get(std::forward<StructT>(obj));
+ } else if constexpr (is_fory_field_v<RawFieldType>) {
+ return (std::forward<StructT>(obj).*entry).value;
+ } else {
+ return (std::forward<StructT>(obj).*entry);
+ }
+}
+
+template <typename StructT, typename Entry, typename Value>
+FORY_ALWAYS_INLINE void field_value_set(StructT &&obj, Entry entry,
+ Value &&value) {
+ using RawFieldType =
+ meta::FieldRawTypeT<std::remove_cv_t<std::remove_reference_t<StructT>>,
+ Entry>;
+ if constexpr (meta::IsPropertyDescriptorV<Entry>) {
+ meta::RemoveCVRefT<Entry>::set(std::forward<StructT>(obj),
+ std::forward<Value>(value));
+ } else if constexpr (is_fory_field_v<RawFieldType>) {
+ (std::forward<StructT>(obj).*entry).value = std::forward<Value>(value);
+ } else {
+ (std::forward<StructT>(obj).*entry) = std::forward<Value>(value);
+ }
+}
+
} // namespace fory
diff --git a/cpp/fory/meta/field_info.h b/cpp/fory/meta/field_info.h
index fb3d8285e..0da5d22b0 100644
--- a/cpp/fory/meta/field_info.h
+++ b/cpp/fory/meta/field_info.h
@@ -320,6 +320,31 @@ constexpr auto make_field_entry(const char *name,
FieldMeta meta) {
namespace meta {
+template <typename T, typename = void>
+struct IsPropertyDescriptor : std::false_type {};
+
+template <typename T>
+struct IsPropertyDescriptor<
+ T, std::void_t<decltype(T::is_fory_property_descriptor)>>
+ : std::bool_constant<T::is_fory_property_descriptor> {};
+
+template <typename T>
+inline constexpr bool IsPropertyDescriptorV =
+ IsPropertyDescriptor<RemoveCVRefT<T>>::value;
+
+template <typename StructT, typename Entry, bool =
IsPropertyDescriptorV<Entry>>
+struct FieldRawType {
+ using Type = RemoveMemberPointerCVRefT<Entry>;
+};
+
+template <typename StructT, typename Entry>
+struct FieldRawType<StructT, Entry, true> {
+ using Type = typename RemoveCVRefT<Entry>::template ForyFieldType<StructT>;
+};
+
+template <typename StructT, typename Entry>
+using FieldRawTypeT = typename FieldRawType<StructT,
RemoveCVRefT<Entry>>::Type;
+
template <typename T> struct Identity {
using Type = T;
};
@@ -366,12 +391,16 @@ constexpr const T &unwrap_tuple(const TupleWrapper<T>
&value) {
// it must be able to be executed in compile-time
template <typename FieldInfo, size_t... I>
constexpr bool is_valid_field_info_impl(std::index_sequence<I...>) {
- if constexpr (sizeof...(I) == 0) {
- return true;
- } else {
- constexpr auto ptrs = FieldInfo::ptrs();
- return IsUnique<std::get<I>(ptrs)...>::value;
+ (void)std::index_sequence<I...>{};
+ constexpr auto names = FieldInfo::Names;
+ for (size_t i = 0; i < FieldInfo::Size; ++i) {
+ for (size_t j = i + 1; j < FieldInfo::Size; ++j) {
+ if (names[i] == names[j]) {
+ return false;
+ }
+ }
}
+ return true;
}
} // namespace details
@@ -390,7 +419,8 @@ struct HasForyStructInfo
// it includes:
// - number of fields: typed size_t
// - field names: typed `std::string_view`
-// - field member points: typed `decltype(a) T::*` for any member `T::a`
+// - field access entries: direct fields use typed `decltype(a) T::*` member
+// pointers; properties use empty compile-time descriptor objects.
template <typename T>
constexpr auto fory_field_info([[maybe_unused]] const T &value) noexcept {
if constexpr (details::HasMemberStructInfo<T>::value) {
@@ -507,20 +537,51 @@ constexpr auto concat_tuples_from_tuple(const Tuple
&tuple) {
#define FORY_BASE_TYPE(arg) FORY_PP_TUPLE_SECOND(arg)
+#define FORY_PROPERTY(...)
\
+ FORY_PP_CONCAT(FORY_PROPERTY_, FORY_PP_NARG(__VA_ARGS__))(__VA_ARGS__)
+
+#define FORY_PROPERTY_1(name) (FORY_PROPERTY_TAG, name, name, name,
::fory::F())
+#define FORY_PROPERTY_2(name, meta) (FORY_PROPERTY_TAG, name, name, name, meta)
+#define FORY_PROPERTY_3(name, getter, setter)
\
+ (FORY_PROPERTY_TAG, name, getter, setter, ::fory::F())
+#define FORY_PROPERTY_4(name, getter, setter, meta)
\
+ (FORY_PROPERTY_TAG, name, getter, setter, meta)
+
+#define FORY_PP_IS_PROPERTY_TAG(x)
\
+ FORY_PP_CHECK(FORY_PP_CONCAT(FORY_PP_IS_PROPERTY_TAG_PROBE_, x))
+#define FORY_PP_IS_PROPERTY_TAG_PROBE_FORY_PROPERTY_TAG FORY_PP_PROBE()
+
+#define FORY_PP_IS_PROPERTY(arg)
\
+ FORY_PP_IS_PROPERTY_IMPL(FORY_PP_IS_PAREN(arg), arg)
+#define FORY_PP_IS_PROPERTY_IMPL(is_paren, arg)
\
+ FORY_PP_CONCAT(FORY_PP_IS_PROPERTY_IMPL_, is_paren)(arg)
+#define FORY_PP_IS_PROPERTY_IMPL_0(arg) 0
+#define FORY_PP_IS_PROPERTY_IMPL_1(arg)
\
+ FORY_PP_IS_PROPERTY_TAG(FORY_PP_TUPLE_FIRST(arg))
+
+#define FORY_PROPERTY_ARG_NAME(arg) FORY_PP_TUPLE_SECOND(arg)
+#define FORY_PROPERTY_ARG_GETTER(arg) FORY_PP_TUPLE_THIRD(arg)
+#define FORY_PROPERTY_ARG_SETTER(arg) FORY_PP_TUPLE_FOURTH(arg)
+#define FORY_PROPERTY_ARG_META(arg) FORY_PP_TUPLE_FIFTH(arg)
+
#define FORY_PP_IS_CONFIG(arg)
\
FORY_PP_IS_CONFIG_IMPL(FORY_PP_IS_PAREN(arg), arg)
#define FORY_PP_IS_CONFIG_IMPL(is_paren, arg)
\
FORY_PP_CONCAT(FORY_PP_IS_CONFIG_IMPL_, is_paren)(arg)
#define FORY_PP_IS_CONFIG_IMPL_0(arg) 0
-#define FORY_PP_IS_CONFIG_IMPL_1(arg) FORY_PP_NOT(FORY_PP_IS_BASE(arg))
+#define FORY_PP_IS_CONFIG_IMPL_1(arg)
\
+ FORY_PP_IF(FORY_PP_IS_BASE(arg))
\
+ (0, FORY_PP_IF(FORY_PP_IS_PROPERTY(arg))(0, 1))
#define FORY_FIELD_ARG_NAME(arg)
\
- FORY_PP_IF(FORY_PP_IS_CONFIG(arg))
\
- (FORY_PP_TUPLE_FIRST(arg), arg)
+ FORY_PP_IF(FORY_PP_IS_PROPERTY(arg))
\
+ (FORY_PROPERTY_ARG_NAME(arg),
\
+ FORY_PP_IF(FORY_PP_IS_CONFIG(arg))(FORY_PP_TUPLE_FIRST(arg), arg))
#define FORY_FIELD_ARG_META(arg)
\
- FORY_PP_IF(FORY_PP_IS_CONFIG(arg))
\
- (FORY_PP_TUPLE_SECOND(arg), ::fory::F())
+ FORY_PP_IF(FORY_PP_IS_PROPERTY(arg))
\
+ (FORY_PROPERTY_ARG_META(arg),
\
+ FORY_PP_IF(FORY_PP_IS_CONFIG(arg))(FORY_PP_TUPLE_SECOND(arg), ::fory::F()))
#define FORY_FIELD_INFO_NAMES_FIELD(field) #field,
#define FORY_FIELD_INFO_NAMES_ARG(arg) FORY_FIELD_INFO_NAMES_FIELD(arg)
@@ -529,11 +590,35 @@ constexpr auto concat_tuples_from_tuple(const Tuple
&tuple) {
(FORY_PP_EMPTY(), FORY_FIELD_INFO_NAMES_ARG(FORY_FIELD_ARG_NAME(arg)))
#define FORY_FIELD_INFO_PTRS_FIELD(type, field) &type::field,
-#define FORY_FIELD_INFO_PTRS_ARG(type, field)
\
- FORY_FIELD_INFO_PTRS_FIELD(type, field)
+#define FORY_PROPERTY_DESCRIPTOR_NAME(arg)
\
+ FORY_PP_CONCAT(ForyPropertyDescriptor_, FORY_PROPERTY_ARG_NAME(arg))
+#define FORY_FIELD_INFO_PROPERTY_DECL(type, arg)
\
+ struct FORY_PROPERTY_DESCRIPTOR_NAME(arg) {
\
+ static constexpr bool is_fory_property_descriptor = true;
\
+ using ForyStructType = type;
\
+ template <typename T>
\
+ using ForyFieldType = ::fory::meta::RemoveCVRefT<
\
+ decltype(std::declval<const T &>().FORY_PROPERTY_ARG_GETTER(arg)())>;
\
+ template <typename Obj> static decltype(auto) get(Obj &&obj) {
\
+ return std::forward<Obj>(obj).FORY_PROPERTY_ARG_GETTER(arg)();
\
+ }
\
+ template <typename Obj, typename Value>
\
+ static void set(Obj &&obj, Value &&value) {
\
+ (void)std::forward<Obj>(obj).FORY_PROPERTY_ARG_SETTER(arg)(
\
+ std::forward<Value>(value));
\
+ }
\
+ };
+#define FORY_FIELD_INFO_PROPERTY_DECL_FUNC(type, arg)
\
+ FORY_PP_IF(FORY_PP_IS_PROPERTY(arg))
\
+ (FORY_FIELD_INFO_PROPERTY_DECL(type, arg), FORY_PP_EMPTY())
+#define FORY_FIELD_INFO_PTRS_PROPERTY(arg)
FORY_PROPERTY_DESCRIPTOR_NAME(arg){},
+#define FORY_FIELD_INFO_PTRS_ARG(type, arg)
\
+ FORY_PP_IF(FORY_PP_IS_PROPERTY(arg))
\
+ (FORY_FIELD_INFO_PTRS_PROPERTY(arg),
\
+ FORY_FIELD_INFO_PTRS_FIELD(type, FORY_FIELD_ARG_NAME(arg)))
#define FORY_FIELD_INFO_PTRS_FUNC(type, arg)
\
FORY_PP_IF(FORY_PP_IS_BASE(arg))
\
- (FORY_PP_EMPTY(), FORY_FIELD_INFO_PTRS_ARG(type, FORY_FIELD_ARG_NAME(arg)))
+ (FORY_PP_EMPTY(), FORY_FIELD_INFO_PTRS_ARG(type, arg))
#define FORY_FIELD_INFO_CONFIG_ENTRY(arg)
\
::fory::detail::make_field_entry(
\
@@ -612,6 +697,7 @@ constexpr auto concat_tuples_from_tuple(const Tuple &tuple)
{
return fory::meta::concat_tuples_from_tuple(
\
std::tuple{FORY_PP_FOREACH(FORY_BASE_PTRS_ARG, __VA_ARGS__)});
\
}
\
+ FORY_PP_FOREACH_1(FORY_FIELD_INFO_PROPERTY_DECL_FUNC, type, __VA_ARGS__)
\
using FieldPtrsType = decltype(std::tuple{
\
FORY_PP_FOREACH_1(FORY_FIELD_INFO_PTRS_FUNC, type, __VA_ARGS__)});
\
static constexpr FieldPtrsType FieldPtrs() {
\
diff --git a/cpp/fory/meta/field_info_test.cc b/cpp/fory/meta/field_info_test.cc
index a873ed00e..a09166f2c 100644
--- a/cpp/fory/meta/field_info_test.cc
+++ b/cpp/fory/meta/field_info_test.cc
@@ -19,7 +19,10 @@
#include "gtest/gtest.h"
+#include "fory/meta/field.h"
#include "fory/meta/field_info.h"
+#include <cstdint>
+#include <type_traits>
namespace fory {
@@ -68,6 +71,52 @@ TEST(FieldInfo, Hidden) {
static_assert(std::get<0>(decltype(info)::ptrs()) == &B::a);
}
+struct C {
+ const int32_t &value() const { return value_; }
+ int32_t &value() { return value_; }
+ C &value(int32_t v) {
+ value_ = v;
+ return *this;
+ }
+
+ const int32_t &get_count() const { return count_; }
+ C &set_count(int32_t v) {
+ count_ = v;
+ return *this;
+ }
+
+ int32_t value_ = 0;
+ int32_t count_ = 0;
+
+ FORY_STRUCT(C, FORY_PROPERTY(value),
+ FORY_PROPERTY(count, get_count, set_count, fory::F(7).varint()));
+};
+
+TEST(FieldInfo, Property) {
+ C c;
+ constexpr auto info = meta::fory_field_info(c);
+
+ static_assert(info.Size == 2);
+ static_assert(info.Names[0] == "value");
+ static_assert(info.Names[1] == "count");
+
+ using ValueEntry = decltype(std::get<0>(decltype(info)::ptrs()));
+ using CountEntry = decltype(std::get<1>(decltype(info)::ptrs()));
+ static_assert(meta::IsPropertyDescriptorV<ValueEntry>);
+ static_assert(meta::IsPropertyDescriptorV<CountEntry>);
+ static_assert(std::is_same_v<meta::FieldRawTypeT<C, ValueEntry>, int32_t>);
+ static_assert(std::is_same_v<meta::FieldRawTypeT<C, CountEntry>, int32_t>);
+ static_assert(std::get<1>(decltype(info)::entries).meta.id_ == 7);
+ static_assert(std::get<1>(decltype(info)::entries).meta.encoding_ ==
+ Encoding::Varint);
+
+ auto entries = decltype(info)::ptrs();
+ field_value_set(c, std::get<0>(entries), 11);
+ field_value_set(c, std::get<1>(entries), 22);
+ EXPECT_EQ(field_value_get(c, std::get<0>(entries)), 11);
+ EXPECT_EQ(field_value_get(c, std::get<1>(entries)), 22);
+}
+
} // namespace test
} // namespace fory
diff --git a/cpp/fory/meta/preprocessor.h b/cpp/fory/meta/preprocessor.h
index 5ebe684ec..64c40fbb6 100644
--- a/cpp/fory/meta/preprocessor.h
+++ b/cpp/fory/meta/preprocessor.h
@@ -97,6 +97,12 @@
#define FORY_PP_TUPLE_FIRST_IMPL(a, ...) a
#define FORY_PP_TUPLE_SECOND(tuple) FORY_PP_TUPLE_SECOND_IMPL tuple
#define FORY_PP_TUPLE_SECOND_IMPL(a, b, ...) b
+#define FORY_PP_TUPLE_THIRD(tuple) FORY_PP_TUPLE_THIRD_IMPL tuple
+#define FORY_PP_TUPLE_THIRD_IMPL(a, b, c, ...) c
+#define FORY_PP_TUPLE_FOURTH(tuple) FORY_PP_TUPLE_FOURTH_IMPL tuple
+#define FORY_PP_TUPLE_FOURTH_IMPL(a, b, c, d, ...) d
+#define FORY_PP_TUPLE_FIFTH(tuple) FORY_PP_TUPLE_FIFTH_IMPL tuple
+#define FORY_PP_TUPLE_FIFTH_IMPL(a, b, c, d, e, ...) e
#define FORY_PP_INVOKE(X, ...) X(__VA_ARGS__)
diff --git a/cpp/fory/serialization/struct_compatible_test.cc
b/cpp/fory/serialization/struct_compatible_test.cc
index da3450eae..b062191ca 100644
--- a/cpp/fory/serialization/struct_compatible_test.cc
+++ b/cpp/fory/serialization/struct_compatible_test.cc
@@ -348,6 +348,52 @@ struct OptionalIntField {
FORY_STRUCT(OptionalIntField, (value, fory::F(1)));
};
+struct PimplCompatibleAImpl {
+ int32_t a = 0;
+};
+
+class PimplCompatibleA {
+public:
+ PimplCompatibleA() : impl_(std::make_unique<PimplCompatibleAImpl>()) {}
+ explicit PimplCompatibleA(int32_t a) : PimplCompatibleA() { impl_->a = a; }
+ PimplCompatibleA(const PimplCompatibleA &other)
+ : PimplCompatibleA(other.a()) {}
+ PimplCompatibleA &operator=(const PimplCompatibleA &other) {
+ a(other.a());
+ return *this;
+ }
+ PimplCompatibleA(PimplCompatibleA &&) noexcept = default;
+ PimplCompatibleA &operator=(PimplCompatibleA &&) noexcept = default;
+
+ const int32_t &a() const { return impl_->a; }
+ int32_t &a() { return impl_->a; }
+ PimplCompatibleA &a(int32_t value) {
+ impl_->a = value;
+ return *this;
+ }
+
+private:
+ std::unique_ptr<PimplCompatibleAImpl> impl_;
+
+public:
+ FORY_STRUCT(PimplCompatibleA, FORY_PROPERTY(a));
+};
+
+struct DirectCompatibleA {
+ int32_t a = 0;
+ FORY_STRUCT(DirectCompatibleA, a);
+};
+
+struct PimplCompatibleB {
+ PimplCompatibleA a;
+ FORY_STRUCT(PimplCompatibleB, a);
+};
+
+struct DirectCompatibleB {
+ DirectCompatibleA a;
+ FORY_STRUCT(DirectCompatibleB, a);
+};
+
// ============================================================================
// TESTS
// ============================================================================
@@ -685,6 +731,31 @@ TEST(SchemaEvolutionTest,
ChangedSchemaReadsExactUnsignedFields) {
EXPECT_EQ(decoded.value().count, value.count);
}
+TEST(SchemaEvolutionTest, PimplPropertyNestedNamedStruct) {
+ auto writer = Fory::builder().compatible(true).xlang(true).build();
+ auto reader = Fory::builder().compatible(true).xlang(true).build();
+
+ ASSERT_TRUE(writer.register_struct<PimplCompatibleA>("pimpl", "A").ok());
+ ASSERT_TRUE(writer.register_struct<PimplCompatibleB>("pimpl", "B").ok());
+ ASSERT_TRUE(reader.register_struct<DirectCompatibleA>("pimpl", "A").ok());
+ ASSERT_TRUE(reader.register_struct<DirectCompatibleB>("pimpl", "B").ok());
+
+ auto type_info = writer.type_resolver().get_type_info<PimplCompatibleA>();
+ ASSERT_TRUE(type_info.ok()) << type_info.error().to_string();
+ EXPECT_EQ(type_info.value()->type_id,
+ static_cast<uint32_t>(TypeId::NAMED_COMPATIBLE_STRUCT));
+
+ PimplCompatibleB original;
+ original.a.a(2468);
+ auto bytes = writer.serialize(original);
+ ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
+
+ auto decoded = reader.deserialize<DirectCompatibleB>(bytes.value().data(),
+ bytes.value().size());
+ ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
+ EXPECT_EQ(decoded.value().a.a, 2468);
+}
+
TEST(SchemaEvolutionTest, ScalarBoolString) {
auto decoded =
convert_field<ScalarStringField, ScalarBoolField>({"true"}, 1010);
diff --git a/cpp/fory/serialization/struct_serializer.h
b/cpp/fory/serialization/struct_serializer.h
index d8a6e2dd2..2954c850e 100644
--- a/cpp/fory/serialization/struct_serializer.h
+++ b/cpp/fory/serialization/struct_serializer.h
@@ -1242,13 +1242,21 @@ template <typename T> struct CompileTimeFieldHelpers {
static inline constexpr auto ptrs = FieldDescriptor::ptrs();
using FieldPtrs = decltype(ptrs);
+ template <size_t Index>
+ using EntryType = std::tuple_element_t<Index, FieldPtrs>;
+ template <size_t Index>
+ using RawFieldTypeFor = meta::FieldRawTypeT<T, EntryType<Index>>;
+ template <size_t Index>
+ using ValueType = unwrap_field_t<RawFieldTypeFor<Index>>;
+ template <size_t Index> static constexpr bool is_direct_field() {
+ return !meta::IsPropertyDescriptorV<EntryType<Index>>;
+ }
+
template <size_t Index> static constexpr uint32_t field_type_id() {
if constexpr (FieldCount == 0) {
return 0;
} else {
- using PtrT = std::tuple_element_t<Index, FieldPtrs>;
- using RawFieldType = meta::RemoveMemberPointerCVRefT<PtrT>;
- using FieldType = unwrap_field_t<RawFieldType>;
+ using FieldType = ValueType<Index>;
if constexpr (::fory::detail::has_field_config_v<T>) {
constexpr uint32_t effective_tid =
@@ -1283,8 +1291,7 @@ template <typename T> struct CompileTimeFieldHelpers {
if constexpr (FieldCount == 0) {
return false;
} else {
- using PtrT = std::tuple_element_t<Index, FieldPtrs>;
- using RawFieldType = meta::RemoveMemberPointerCVRefT<PtrT>;
+ using RawFieldType = RawFieldTypeFor<Index>;
if constexpr (is_fory_field_v<RawFieldType>) {
return RawFieldType::is_nullable;
@@ -1312,8 +1319,7 @@ template <typename T> struct CompileTimeFieldHelpers {
if constexpr (FieldCount == 0) {
return -1;
} else {
- using PtrT = std::tuple_element_t<Index, FieldPtrs>;
- using RawFieldType = meta::RemoveMemberPointerCVRefT<PtrT>;
+ using RawFieldType = RawFieldTypeFor<Index>;
if constexpr (::fory::detail::has_field_config_v<T>) {
constexpr int16_t config_id =
@@ -1351,8 +1357,7 @@ template <typename T> struct CompileTimeFieldHelpers {
if constexpr (FieldCount == 0) {
return false;
} else {
- using PtrT = std::tuple_element_t<Index, FieldPtrs>;
- using RawFieldType = meta::RemoveMemberPointerCVRefT<PtrT>;
+ using RawFieldType = RawFieldTypeFor<Index>;
if constexpr (is_fory_field_v<RawFieldType>) {
return RawFieldType::track_ref;
@@ -1379,8 +1384,7 @@ template <typename T> struct CompileTimeFieldHelpers {
if constexpr (FieldCount == 0) {
return -1; // AUTO
} else {
- using PtrT = std::tuple_element_t<Index, FieldPtrs>;
- using RawFieldType = meta::RemoveMemberPointerCVRefT<PtrT>;
+ using RawFieldType = RawFieldTypeFor<Index>;
if constexpr (is_fory_field_v<RawFieldType>) {
return RawFieldType::dynamic_value;
@@ -1408,9 +1412,7 @@ template <typename T> struct CompileTimeFieldHelpers {
if constexpr (FieldCount == 0) {
return false;
} else {
- using PtrT = std::tuple_element_t<Index, FieldPtrs>;
- using RawFieldType = meta::RemoveMemberPointerCVRefT<PtrT>;
- using FieldType = unwrap_field_t<RawFieldType>;
+ using FieldType = ValueType<Index>;
constexpr TypeId field_type_id = Serializer<FieldType>::type_id;
constexpr bool is_struct = is_struct_type(field_type_id);
@@ -1442,9 +1444,7 @@ template <typename T> struct CompileTimeFieldHelpers {
/// get the underlying field type.
template <size_t Index> struct UnwrappedFieldTypeHelper {
- using PtrT = std::tuple_element_t<Index, FieldPtrs>;
- using RawFieldType = meta::RemoveMemberPointerCVRefT<PtrT>;
- using type = unwrap_field_t<RawFieldType>;
+ using type = ValueType<Index>;
};
template <size_t Index>
using UnwrappedFieldType = typename UnwrappedFieldTypeHelper<Index>::type;
@@ -1456,9 +1456,7 @@ template <typename T> struct CompileTimeFieldHelpers {
if constexpr (FieldCount == 0) {
return false;
} else {
- using PtrT = std::tuple_element_t<Index, FieldPtrs>;
- using RawFieldType = meta::RemoveMemberPointerCVRefT<PtrT>;
- using FieldType = unwrap_field_t<RawFieldType>;
+ using FieldType = ValueType<Index>;
// Check the unwrapped type
return is_nullable_v<FieldType>;
}
@@ -1472,9 +1470,10 @@ template <typename T> struct CompileTimeFieldHelpers {
if constexpr (FieldCount == 0) {
return false;
} else {
- using PtrT = std::tuple_element_t<Index, FieldPtrs>;
- using RawFieldType = meta::RemoveMemberPointerCVRefT<PtrT>;
- using FieldType = unwrap_field_t<RawFieldType>;
+ if constexpr (!is_direct_field<Index>()) {
+ return false;
+ }
+ using FieldType = ValueType<Index>;
if constexpr (is_configurable_int_v<FieldType>) {
return configurable_int_is_fixed<FieldType, T, Index>();
@@ -1498,9 +1497,10 @@ template <typename T> struct CompileTimeFieldHelpers {
if constexpr (FieldCount == 0) {
return false;
} else {
- using PtrT = std::tuple_element_t<Index, FieldPtrs>;
- using RawFieldType = meta::RemoveMemberPointerCVRefT<PtrT>;
- using FieldType = unwrap_field_t<RawFieldType>;
+ if constexpr (!is_direct_field<Index>()) {
+ return false;
+ }
+ using FieldType = ValueType<Index>;
if constexpr (is_configurable_int_v<FieldType>) {
return configurable_int_is_varint<FieldType, T, Index>();
@@ -1518,9 +1518,10 @@ template <typename T> struct CompileTimeFieldHelpers {
if constexpr (FieldCount == 0) {
return 0;
} else {
- using PtrT = std::tuple_element_t<Index, FieldPtrs>;
- using RawFieldType = meta::RemoveMemberPointerCVRefT<PtrT>;
- using FieldType = unwrap_field_t<RawFieldType>;
+ if constexpr (!is_direct_field<Index>()) {
+ return 0;
+ }
+ using FieldType = ValueType<Index>;
if constexpr (std::is_same_v<FieldType, bool> ||
std::is_same_v<FieldType, int8_t> ||
std::is_same_v<FieldType, uint8_t>) {
@@ -1547,9 +1548,10 @@ template <typename T> struct CompileTimeFieldHelpers {
if constexpr (FieldCount == 0) {
return 0;
} else {
- using PtrT = std::tuple_element_t<Index, FieldPtrs>;
- using RawFieldType = meta::RemoveMemberPointerCVRefT<PtrT>;
- using FieldType = unwrap_field_t<RawFieldType>;
+ if constexpr (!is_direct_field<Index>()) {
+ return 0;
+ }
+ using FieldType = ValueType<Index>;
if constexpr (is_configurable_int_v<FieldType>) {
return configurable_int_max_varint_bytes<FieldType, T, Index>();
@@ -1565,6 +1567,16 @@ template <typename T> struct CompileTimeFieldHelpers {
}
/// Create arrays of field encoding info at compile time
+ template <size_t... Indices>
+ static constexpr std::array<bool, FieldCount>
+ make_direct_field_array(std::index_sequence<Indices...>) {
+ if constexpr (FieldCount == 0) {
+ return {};
+ } else {
+ return {is_direct_field<Indices>()...};
+ }
+ }
+
template <size_t... Indices>
static constexpr std::array<bool, FieldCount>
make_field_is_fixed_array(std::index_sequence<Indices...>) {
@@ -1607,6 +1619,8 @@ template <typename T> struct CompileTimeFieldHelpers {
/// Arrays storing encoding info for each field (indexed by original field
/// index)
+ static inline constexpr std::array<bool, FieldCount> direct_fields =
+ make_direct_field_array(std::make_index_sequence<FieldCount>{});
static inline constexpr std::array<bool, FieldCount> field_is_fixed =
make_field_is_fixed_array(std::make_index_sequence<FieldCount>{});
static inline constexpr std::array<bool, FieldCount> field_is_varint =
@@ -1848,7 +1862,7 @@ template <typename T> struct CompileTimeFieldHelpers {
} else {
uint32_t tid = type_ids[index];
bool nullable = nullable_flags[index];
- if (is_primitive_type_id(tid)) {
+ if (direct_fields[index] && is_primitive_type_id(tid)) {
return nullable ? 1 : 0;
}
return 2;
@@ -1969,8 +1983,8 @@ template <typename T> struct CompileTimeFieldHelpers {
return true;
} else {
for (size_t i = 0; i < FieldCount; ++i) {
- if (!is_primitive_type_id(type_ids[i]) || nullable_flags[i] ||
- nullable_type_flags[i]) {
+ if (!direct_fields[i] || !is_primitive_type_id(type_ids[i]) ||
+ nullable_flags[i] || nullable_type_flags[i]) {
return false;
}
}
@@ -2045,7 +2059,8 @@ template <typename T> struct CompileTimeFieldHelpers {
size_t count = 0;
for (size_t i = 0; i < FieldCount; ++i) {
size_t original_idx = sorted_indices[i];
- if (is_primitive_type_id(type_ids[original_idx]) &&
+ if (direct_fields[original_idx] &&
+ is_primitive_type_id(type_ids[original_idx]) &&
!nullable_flags[original_idx] &&
!nullable_type_flags[original_idx]) {
++count;
@@ -2313,10 +2328,10 @@ FORY_ALWAYS_INLINE void write_single_fixed_field(const
T &obj, Buffer &buffer,
const auto field_info = fory_field_info(obj);
const auto field_ptr =
std::get<original_index>(decltype(field_info)::ptrs_ref());
+ static_assert(Helpers::template is_direct_field<original_index>());
using RawFieldType =
typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
using FieldType = unwrap_field_t<RawFieldType>;
- // get the actual field value.
const FieldType &field_value = [&]() -> const FieldType & {
if constexpr (is_fory_field_v<RawFieldType>) {
return (obj.*field_ptr).value;
@@ -2355,10 +2370,10 @@ FORY_ALWAYS_INLINE void write_single_varint_field(const
T &obj, Buffer &buffer,
const auto field_info = fory_field_info(obj);
const auto field_ptr =
std::get<original_index>(decltype(field_info)::ptrs_ref());
+ static_assert(Helpers::template is_direct_field<original_index>());
using RawFieldType =
typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
using FieldType = unwrap_field_t<RawFieldType>;
- // get the actual field value.
const FieldType &field_value = [&]() -> const FieldType & {
if constexpr (is_fory_field_v<RawFieldType>) {
return (obj.*field_ptr).value;
@@ -2398,10 +2413,10 @@ write_single_remaining_field(const T &obj, Buffer
&buffer, uint32_t &offset) {
const auto field_info = fory_field_info(obj);
const auto field_ptr =
std::get<original_index>(decltype(field_info)::ptrs_ref());
+ static_assert(Helpers::template is_direct_field<original_index>());
using RawFieldType =
typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
using FieldType = unwrap_field_t<RawFieldType>;
- // get the actual field value.
const FieldType &field_value = [&]() -> const FieldType & {
if constexpr (is_fory_field_v<RawFieldType>) {
return (obj.*field_ptr).value;
@@ -2481,18 +2496,19 @@ template <typename T, size_t Index, typename FieldPtrs>
void write_single_field(const T &obj, WriteContext &ctx,
const FieldPtrs &field_ptrs, bool has_generics) {
using Helpers = CompileTimeFieldHelpers<T>;
- const auto field_ptr = std::get<Index>(field_ptrs);
- using RawFieldType =
- typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
+ const auto field_entry = std::get<Index>(field_ptrs);
+ using RawFieldType = meta::FieldRawTypeT<T, decltype(field_entry)>;
using FieldType = unwrap_field_t<RawFieldType>;
- // get the actual field value.
- const auto &raw_field_ref = obj.*field_ptr;
- const FieldType &field_value = [&]() -> const FieldType & {
- if constexpr (is_fory_field_v<RawFieldType>) {
- return raw_field_ref.value;
+ auto &&field_value = [&]() -> decltype(auto) {
+ if constexpr (Helpers::template is_direct_field<Index>()) {
+ if constexpr (is_fory_field_v<RawFieldType>) {
+ return (obj.*field_entry).value;
+ } else {
+ return (obj.*field_entry);
+ }
} else {
- return raw_field_ref;
+ return field_value_get(obj, field_entry);
}
}();
@@ -2956,9 +2972,8 @@ void read_single_field_by_index(T &obj, ReadContext &ctx)
{
using Helpers = CompileTimeFieldHelpers<T>;
const auto field_info = fory_field_info(obj);
const auto &field_ptrs = decltype(field_info)::ptrs_ref();
- const auto field_ptr = std::get<Index>(field_ptrs);
- using RawFieldType =
- typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
+ const auto field_entry = std::get<Index>(field_ptrs);
+ using RawFieldType = meta::FieldRawTypeT<T, decltype(field_entry)>;
using FieldType = unwrap_field_t<RawFieldType>;
// In non-compatible mode, no type info for fields except for polymorphic
@@ -3004,11 +3019,7 @@ void read_single_field_by_index(T &obj, ReadContext
&ctx) {
if constexpr (configured_node_has_override<T, Index>()) {
FieldType result = read_configured_value<FieldType, T, Index, 0>(
ctx, field_ref_mode, read_type);
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::move(result);
- } else {
- obj.*field_ptr = std::move(result);
- }
+ field_value_set(obj, field_entry, std::move(result));
return;
}
@@ -3024,12 +3035,7 @@ void read_single_field_by_index(T &obj, ReadContext
&ctx) {
} else {
value = read_primitive_field_direct<FieldType>(ctx, ctx.error());
}
- // Assign to field.
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = value;
- } else {
- obj.*field_ptr = value;
- }
+ field_value_set(obj, field_entry, value);
} else {
// Special handling for std::optional<uint32_t/uint64_t> with encoding
// config
@@ -3053,11 +3059,7 @@ void read_single_field_by_index(T &obj, ReadContext
&ctx) {
return;
}
if (flag == NULL_FLAG) {
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::nullopt;
- } else {
- obj.*field_ptr = std::nullopt;
- }
+ field_value_set(obj, field_entry, std::nullopt);
return;
}
// Read the value with encoding-aware reading
@@ -3078,11 +3080,7 @@ void read_single_field_by_index(T &obj, ReadContext
&ctx) {
value = ctx.read_uint64(ctx.error());
}
}
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::optional<InnerType>(value);
- } else {
- obj.*field_ptr = std::optional<InnerType>(value);
- }
+ field_value_set(obj, field_entry, std::optional<InnerType>(value));
} else if constexpr (is_encoded_optional_int) {
constexpr auto enc =
::fory::detail::GetFieldConfigEntry<T, Index>::encoding;
@@ -3091,11 +3089,7 @@ void read_single_field_by_index(T &obj, ReadContext
&ctx) {
return;
}
if (flag == NULL_FLAG) {
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::nullopt;
- } else {
- obj.*field_ptr = std::nullopt;
- }
+ field_value_set(obj, field_entry, std::nullopt);
return;
}
using InnerType = typename
std::remove_reference_t<FieldType>::value_type;
@@ -3117,20 +3111,12 @@ void read_single_field_by_index(T &obj, ReadContext
&ctx) {
value = static_cast<InnerType>(ctx.read_var_int64(ctx.error()));
}
}
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::optional<InnerType>(value);
- } else {
- obj.*field_ptr = std::optional<InnerType>(value);
- }
+ field_value_set(obj, field_entry, std::optional<InnerType>(value));
} else {
// Assign to field.
FieldType result =
Serializer<FieldType>::read(ctx, field_ref_mode, read_type);
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::move(result);
- } else {
- obj.*field_ptr = std::move(result);
- }
+ field_value_set(obj, field_entry, std::move(result));
}
}
}
@@ -3144,9 +3130,8 @@ void read_single_field_by_index_compatible(T &obj,
ReadContext &ctx,
using Helpers = CompileTimeFieldHelpers<T>;
const auto field_info = fory_field_info(obj);
const auto &field_ptrs = decltype(field_info)::ptrs_ref();
- const auto field_ptr = std::get<Index>(field_ptrs);
- using RawFieldType =
- typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
+ const auto field_entry = std::get<Index>(field_ptrs);
+ using RawFieldType = meta::FieldRawTypeT<T, decltype(field_entry)>;
using FieldType = unwrap_field_t<RawFieldType>;
const RefMode remote_ref_mode = remote_field_type.ref_mode;
const uint32_t remote_type_id = remote_field_type.type_id;
@@ -3206,11 +3191,7 @@ void read_single_field_by_index_compatible(T &obj,
ReadContext &ctx,
if (FORY_PREDICT_FALSE(ctx.has_error())) {
return;
}
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::move(result);
- } else {
- obj.*field_ptr = std::move(result);
- }
+ field_value_set(obj, field_entry, std::move(result));
return;
}
}
@@ -3229,11 +3210,7 @@ void read_single_field_by_index_compatible(T &obj,
ReadContext &ctx,
return;
}
if (!has_value) {
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::nullopt;
- } else {
- obj.*field_ptr = std::nullopt;
- }
+ field_value_set(obj, field_entry, std::nullopt);
return;
}
}
@@ -3242,11 +3219,8 @@ void read_single_field_by_index_compatible(T &obj,
ReadContext &ctx,
if (FORY_PREDICT_FALSE(ctx.has_error())) {
return;
}
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::optional<InnerType>(std::move(value));
- } else {
- obj.*field_ptr = std::optional<InnerType>(std::move(value));
- }
+ field_value_set(obj, field_entry,
+ std::optional<InnerType>(std::move(value)));
return;
}
}
@@ -3258,13 +3232,9 @@ void read_single_field_by_index_compatible(T &obj,
ReadContext &ctx,
if constexpr (is_raw_prim && is_primitive_field) {
if (remote_ref_mode == RefMode::None) {
// Remote is non-nullable, no ref flag
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = read_primitive_by_type_id<FieldType>(
- ctx, remote_type_id, ctx.error());
- } else {
- obj.*field_ptr = read_primitive_by_type_id<FieldType>(
- ctx, remote_type_id, ctx.error());
- }
+ FieldType value = read_primitive_by_type_id<FieldType>(
+ ctx, remote_type_id, ctx.error());
+ field_value_set(obj, field_entry, std::move(value));
return;
} else {
// Remote is nullable, has ref flag
@@ -3279,13 +3249,9 @@ void read_single_field_by_index_compatible(T &obj,
ReadContext &ctx,
return;
}
// NOT_NULL_VALUE_FLAG or REF_VALUE_FLAG - read the value
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = read_primitive_by_type_id<FieldType>(
- ctx, remote_type_id, ctx.error());
- } else {
- obj.*field_ptr = read_primitive_by_type_id<FieldType>(
- ctx, remote_type_id, ctx.error());
- }
+ FieldType value = read_primitive_by_type_id<FieldType>(
+ ctx, remote_type_id, ctx.error());
+ field_value_set(obj, field_entry, std::move(value));
return;
}
}
@@ -3304,11 +3270,7 @@ void read_single_field_by_index_compatible(T &obj,
ReadContext &ctx,
if (FORY_PREDICT_FALSE(ctx.has_error())) {
return;
}
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::optional<InnerType>(value);
- } else {
- obj.*field_ptr = std::optional<InnerType>(value);
- }
+ field_value_set(obj, field_entry, std::optional<InnerType>(value));
return;
} else {
// Remote is nullable, has ref flag
@@ -3318,11 +3280,7 @@ void read_single_field_by_index_compatible(T &obj,
ReadContext &ctx,
}
if (flag == NULL_FLAG) {
// Null value - set optional to nullopt
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::nullopt;
- } else {
- obj.*field_ptr = std::nullopt;
- }
+ field_value_set(obj, field_entry, std::nullopt);
return;
}
// NOT_NULL_VALUE_FLAG or REF_VALUE_FLAG - read the value
@@ -3331,11 +3289,7 @@ void read_single_field_by_index_compatible(T &obj,
ReadContext &ctx,
if (FORY_PREDICT_FALSE(ctx.has_error())) {
return;
}
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::optional<InnerType>(value);
- } else {
- obj.*field_ptr = std::optional<InnerType>(value);
- }
+ field_value_set(obj, field_entry, std::optional<InnerType>(value));
return;
}
}
@@ -3358,11 +3312,7 @@ void read_single_field_by_index_compatible(T &obj,
ReadContext &ctx,
constexpr int8_t child = configured_node_child<T, Index, 0, 0>();
FieldType result = read_configured_list_data_as_array_field<
FieldType, T, Index, 0, child>(ctx, remote_element_type.type_id);
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::move(result);
- } else {
- obj.*field_ptr = std::move(result);
- }
+ field_value_set(obj, field_entry, std::move(result));
return;
}
} else if constexpr (configured_as_list) {
@@ -3370,11 +3320,7 @@ void read_single_field_by_index_compatible(T &obj,
ReadContext &ctx,
if (primitive_array_element_type_id(remote_type_id, element_type_id)) {
FieldType result = read_configured_array_data_as_list_field<FieldType>(
ctx, remote_ref_mode);
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::move(result);
- } else {
- obj.*field_ptr = std::move(result);
- }
+ field_value_set(obj, field_entry, std::move(result));
return;
}
}
@@ -3383,22 +3329,14 @@ void read_single_field_by_index_compatible(T &obj,
ReadContext &ctx,
if constexpr (configured_node_has_override<T, Index>()) {
FieldType result = read_configured_value<FieldType, T, Index, 0>(
ctx, remote_ref_mode, read_type);
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::move(result);
- } else {
- obj.*field_ptr = std::move(result);
- }
+ field_value_set(obj, field_entry, std::move(result));
return;
}
// For non-primitive types, use the standard serializer path
FieldType result =
Serializer<FieldType>::read(ctx, remote_ref_mode, read_type);
- if constexpr (is_fory_field_v<RawFieldType>) {
- (obj.*field_ptr).value = std::move(result);
- } else {
- obj.*field_ptr = std::move(result);
- }
+ field_value_set(obj, field_entry, std::move(result));
}
template <typename T, size_t MatchedId>
@@ -3417,13 +3355,12 @@ template <typename T, size_t Index>
constexpr bool can_read_exact_primitive_with_offset() {
using Helpers = CompileTimeFieldHelpers<T>;
constexpr size_t original_index = Helpers::sorted_indices[Index];
- using FieldPtr =
- typename std::tuple_element<original_index,
- typename Helpers::FieldPtrs>::type;
- using RawFieldType = typename meta::RemoveMemberPointerCVRefT<FieldPtr>;
+ using RawFieldType =
+ typename Helpers::template RawFieldTypeFor<original_index>;
using FieldType = unwrap_field_t<RawFieldType>;
constexpr TypeId field_type_id = Serializer<FieldType>::type_id;
- return is_raw_primitive_v<FieldType> && is_primitive_type_id(field_type_id)
&&
+ return Helpers::template is_direct_field<original_index>() &&
+ is_raw_primitive_v<FieldType> && is_primitive_type_id(field_type_id)
&&
!is_nullable_v<FieldType> &&
!Helpers::template field_nullable<original_index>() &&
!configured_node_has_override<T, original_index>();
@@ -3437,6 +3374,7 @@ FORY_ALWAYS_INLINE void read_single_exact_primitive_at(T
&obj, ReadContext &ctx,
const auto field_info = fory_field_info(obj);
const auto field_ptr =
std::get<original_index>(decltype(field_info)::ptrs_ref());
+ static_assert(Helpers::template is_direct_field<original_index>());
using RawFieldType =
typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
using FieldType = unwrap_field_t<RawFieldType>;
@@ -3848,12 +3786,12 @@ FORY_ALWAYS_INLINE void read_single_fixed_field(T &obj,
Buffer &buffer,
const auto field_info = fory_field_info(obj);
const auto field_ptr =
std::get<original_index>(decltype(field_info)::ptrs_ref());
+ static_assert(Helpers::template is_direct_field<original_index>());
using RawFieldType =
typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
using FieldType = unwrap_field_t<RawFieldType>;
FieldType result =
read_fixed_primitive_at<FieldType>(buffer, base_offset + field_offset);
- // Assign to field.
if constexpr (is_fory_field_v<RawFieldType>) {
(obj.*field_ptr).value = result;
} else {
@@ -4084,6 +4022,7 @@ FORY_ALWAYS_INLINE void read_single_varint_field(T &obj,
Buffer &buffer,
const auto field_info = fory_field_info(obj);
const auto field_ptr =
std::get<original_index>(decltype(field_info)::ptrs_ref());
+ static_assert(Helpers::template is_direct_field<original_index>());
using RawFieldType =
typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
using FieldType = unwrap_field_t<RawFieldType>;
@@ -4096,7 +4035,6 @@ FORY_ALWAYS_INLINE void read_single_varint_field(T &obj,
Buffer &buffer,
result = read_varint_at<FieldType>(buffer, offset);
}
- // Assign to field.
if constexpr (is_fory_field_v<RawFieldType>) {
(obj.*field_ptr).value = result;
} else {
diff --git a/cpp/fory/serialization/struct_test.cc
b/cpp/fory/serialization/struct_test.cc
index 970cf7154..0d2ba527d 100644
--- a/cpp/fory/serialization/struct_test.cc
+++ b/cpp/fory/serialization/struct_test.cc
@@ -37,6 +37,7 @@
#include <climits>
#include <limits>
#include <map>
+#include <memory>
#include <optional>
#include <string>
#include <vector>
@@ -165,6 +166,74 @@ public:
FORY_STRUCT(PrivateFieldsStruct, id_, name_, scores_);
};
+struct PimplPropertyImpl {
+ int32_t a = 0;
+};
+
+class PimplPropertyStruct {
+public:
+ PimplPropertyStruct() : impl_(std::make_unique<PimplPropertyImpl>()) {}
+ explicit PimplPropertyStruct(int32_t a) : PimplPropertyStruct() {
+ impl_->a = a;
+ }
+ PimplPropertyStruct(const PimplPropertyStruct &other)
+ : PimplPropertyStruct(other.a()) {}
+ PimplPropertyStruct &operator=(const PimplPropertyStruct &other) {
+ a(other.a());
+ return *this;
+ }
+ PimplPropertyStruct(PimplPropertyStruct &&) noexcept = default;
+ PimplPropertyStruct &operator=(PimplPropertyStruct &&) noexcept = default;
+
+ const int32_t &a() const { return impl_->a; }
+ int32_t &a() { return impl_->a; }
+ PimplPropertyStruct &a(int32_t value) {
+ impl_->a = value;
+ return *this;
+ }
+
+ bool operator==(const PimplPropertyStruct &other) const {
+ return a() == other.a();
+ }
+
+private:
+ std::unique_ptr<PimplPropertyImpl> impl_;
+
+public:
+ FORY_STRUCT(PimplPropertyStruct, FORY_PROPERTY(a));
+};
+
+struct PimplConfiguredPropertyImpl {
+ int32_t value = 0;
+};
+
+class PimplConfiguredPropertyStruct {
+public:
+ PimplConfiguredPropertyStruct()
+ : impl_(std::make_unique<PimplConfiguredPropertyImpl>()) {}
+ explicit PimplConfiguredPropertyStruct(int32_t value)
+ : PimplConfiguredPropertyStruct() {
+ impl_->value = value;
+ }
+
+ const int32_t &get_value() const { return impl_->value; }
+ PimplConfiguredPropertyStruct &set_value(int32_t value) {
+ impl_->value = value;
+ return *this;
+ }
+
+ bool operator==(const PimplConfiguredPropertyStruct &other) const {
+ return get_value() == other.get_value();
+ }
+
+private:
+ std::unique_ptr<PimplConfiguredPropertyImpl> impl_;
+
+public:
+ FORY_STRUCT(PimplConfiguredPropertyStruct,
+ FORY_PROPERTY(value, get_value, set_value, fory::F(1).varint()));
+};
+
// All primitives
struct AllPrimitivesStruct {
bool bool_val;
@@ -536,6 +605,8 @@ inline void register_all_test_types(Fory &fory) {
fory.register_struct<NumericTaggedOrderStruct>(type_id++);
fory.register_struct<TaggedGroupedOrderStruct>(type_id++);
fory.register_struct<PrivateFieldsStruct>(type_id++);
+ fory.register_struct<PimplPropertyStruct>(type_id++);
+ fory.register_struct<PimplConfiguredPropertyStruct>(type_id++);
fory.register_struct<AllPrimitivesStruct>(type_id++);
fory.register_struct<StringTestStruct>(type_id++);
fory.register_struct<Point2D>(type_id++);
@@ -629,6 +700,14 @@ TEST(StructComprehensiveTest, PrivateFieldsStruct) {
test_roundtrip(PrivateFieldsStruct{42, "secret", {1, 2, 3}});
}
+TEST(StructComprehensiveTest, PimplPropertyStruct) {
+ test_roundtrip(PimplPropertyStruct{12345});
+}
+
+TEST(StructComprehensiveTest, PimplConfiguredPropertyStruct) {
+ test_roundtrip(PimplConfiguredPropertyStruct{-12345});
+}
+
TEST(StructComprehensiveTest, AllPrimitivesZero) {
test_roundtrip(AllPrimitivesStruct{false, 0, 0, 0, 0, 0, 0, 0, 0, 0.0f,
0.0});
}
diff --git a/cpp/fory/serialization/type_resolver.h
b/cpp/fory/serialization/type_resolver.h
index 85774d168..5e6ec218b 100644
--- a/cpp/fory/serialization/type_resolver.h
+++ b/cpp/fory/serialization/type_resolver.h
@@ -1175,9 +1175,8 @@ template <typename T, size_t Index> struct
FieldInfoBuilder {
::fory::to_snake_case<max_snake_len>(original_name);
std::string field_name(snake_buffer.data(), snake_len);
- const auto field_ptr = std::get<Index>(field_ptrs);
- using RawFieldType =
- typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
+ const auto field_entry = std::get<Index>(field_ptrs);
+ using RawFieldType = meta::FieldRawTypeT<T, decltype(field_entry)>;
using ActualFieldType =
std::remove_cv_t<std::remove_reference_t<RawFieldType>>;
using UnwrappedFieldType = fory::unwrap_field_t<ActualFieldType>;
@@ -1222,9 +1221,8 @@ template <typename T, size_t Index> struct
FieldInfoBuilder {
::fory::to_snake_case<max_snake_len>(original_name);
std::string field_name(snake_buffer.data(), snake_len);
- const auto field_ptr = std::get<Index>(field_ptrs);
- using RawFieldType =
- typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
+ const auto field_entry = std::get<Index>(field_ptrs);
+ using RawFieldType = meta::FieldRawTypeT<T, decltype(field_entry)>;
using ActualFieldType =
std::remove_cv_t<std::remove_reference_t<RawFieldType>>;
using UnwrappedFieldType = fory::unwrap_field_t<ActualFieldType>;
diff --git a/docs/benchmarks/cpp/README.md b/docs/benchmarks/cpp/README.md
index f8fde1a79..8b201fb77 100644
--- a/docs/benchmarks/cpp/README.md
+++ b/docs/benchmarks/cpp/README.md
@@ -1,6 +1,6 @@
# C++ Benchmark Performance Report
-_Generated on 2026-05-08 17:54:45_
+_Generated on 2026-06-12 16:14:04_
## How to Generate This Report
@@ -27,7 +27,7 @@ The plot shows throughput (ops/sec); higher is better.
| CPU Cores (Physical) | 12 |
| CPU Cores (Logical) | 12 |
| Total RAM (GB) | 48.0 |
-| Benchmark Date | 2026-05-08T16:29:28+08:00 |
+| Benchmark Date | 2026-06-12T16:13:27+08:00 |
| CPU Cores (from benchmark) | 12 |
## Benchmark Results
@@ -36,35 +36,35 @@ The plot shows throughput (ops/sec); higher is better.
| Datatype | Operation | fory (ns) | protobuf (ns) | msgpack (ns) |
Fastest |
| ----------------- | ----------- | --------- | ------------- | ------------ |
------- |
-| NumericStruct | Serialize | 24.9 | 48.2 | 91.0 |
fory |
-| NumericStruct | Deserialize | 26.6 | 33.0 | 1194.5 |
fory |
-| Sample | Serialize | 62.3 | 97.3 | 314.6 |
fory |
-| Sample | Deserialize | 371.1 | 689.0 | 2649.9 |
fory |
-| MediaContent | Serialize | 115.0 | 857.2 | 311.7 |
fory |
-| MediaContent | Deserialize | 406.5 | 1193.1 | 3311.1 |
fory |
-| NumericStructList | Serialize | 81.7 | 495.0 | 485.6 |
fory |
-| NumericStructList | Deserialize | 180.9 | 410.6 | 5733.1 |
fory |
-| SampleList | Serialize | 284.9 | 5004.9 | 1579.6 |
fory |
-| SampleList | Deserialize | 1928.7 | 5118.1 | 13396.8 |
fory |
-| MediaContentList | Serialize | 464.8 | 4861.1 | 1671.1 |
fory |
-| MediaContentList | Deserialize | 2099.8 | 6610.3 | 13963.4 |
fory |
+| NumericStruct | Serialize | 25.7 | 48.2 | 85.4 |
fory |
+| NumericStruct | Deserialize | 25.1 | 31.8 | 887.2 |
fory |
+| Sample | Serialize | 60.6 | 96.4 | 361.6 |
fory |
+| Sample | Deserialize | 176.7 | 397.0 | 2031.6 |
fory |
+| MediaContent | Serialize | 113.5 | 471.0 | 290.4 |
fory |
+| MediaContent | Deserialize | 247.3 | 641.9 | 2015.4 |
fory |
+| NumericStructList | Serialize | 83.3 | 372.9 | 446.9 |
fory |
+| NumericStructList | Deserialize | 158.1 | 268.2 | 4342.4 |
fory |
+| SampleList | Serialize | 258.8 | 2829.7 | 2602.3 |
fory |
+| SampleList | Deserialize | 1001.7 | 2794.4 | 12220.7 |
fory |
+| MediaContentList | Serialize | 504.2 | 2589.8 | 1549.5 |
fory |
+| MediaContentList | Deserialize | 1258.6 | 3620.3 | 10263.4 |
fory |
### Throughput Results (ops/sec)
| Datatype | Operation | fory TPS | protobuf TPS | msgpack TPS |
Fastest |
| ----------------- | ----------- | ---------- | ------------ | ----------- |
------- |
-| NumericStruct | Serialize | 40,087,668 | 20,733,305 | 10,989,907 |
fory |
-| NumericStruct | Deserialize | 37,606,127 | 30,296,744 | 837,189 |
fory |
-| Sample | Serialize | 16,041,299 | 10,277,207 | 3,178,983 |
fory |
-| Sample | Deserialize | 2,694,434 | 1,451,449 | 377,373 |
fory |
-| MediaContent | Serialize | 8,698,574 | 1,166,539 | 3,208,626 |
fory |
-| MediaContent | Deserialize | 2,460,094 | 838,185 | 302,013 |
fory |
-| NumericStructList | Serialize | 12,240,275 | 2,020,102 | 2,059,276 |
fory |
-| NumericStructList | Deserialize | 5,527,333 | 2,435,246 | 174,427 |
fory |
-| SampleList | Serialize | 3,510,210 | 199,804 | 633,061 |
fory |
-| SampleList | Deserialize | 518,490 | 195,386 | 74,645 |
fory |
-| MediaContentList | Serialize | 2,151,560 | 205,715 | 598,396 |
fory |
-| MediaContentList | Deserialize | 476,241 | 151,280 | 71,616 |
fory |
+| NumericStruct | Serialize | 38,845,461 | 20,734,963 | 11,707,994 |
fory |
+| NumericStruct | Deserialize | 39,872,217 | 31,443,829 | 1,127,092 |
fory |
+| Sample | Serialize | 16,496,488 | 10,372,657 | 2,765,312 |
fory |
+| Sample | Deserialize | 5,660,852 | 2,518,926 | 492,232 |
fory |
+| MediaContent | Serialize | 8,808,084 | 2,122,926 | 3,443,519 |
fory |
+| MediaContent | Deserialize | 4,043,028 | 1,557,819 | 496,175 |
fory |
+| NumericStructList | Serialize | 11,999,598 | 2,681,661 | 2,237,536 |
fory |
+| NumericStructList | Deserialize | 6,323,730 | 3,728,133 | 230,285 |
fory |
+| SampleList | Serialize | 3,864,068 | 353,391 | 384,276 |
fory |
+| SampleList | Deserialize | 998,326 | 357,854 | 81,828 |
fory |
+| MediaContentList | Serialize | 1,983,502 | 386,135 | 645,372 |
fory |
+| MediaContentList | Deserialize | 794,544 | 276,221 | 97,434 |
fory |
### Serialized Data Sizes (bytes)
diff --git a/docs/benchmarks/cpp/throughput.png
b/docs/benchmarks/cpp/throughput.png
index 4ae6119fa..b2d6bfffb 100644
Binary files a/docs/benchmarks/cpp/throughput.png and
b/docs/benchmarks/cpp/throughput.png differ
diff --git a/docs/guide/cpp/basic-serialization.md
b/docs/guide/cpp/basic-serialization.md
index 3a1647b3d..b4f9c5893 100644
--- a/docs/guide/cpp/basic-serialization.md
+++ b/docs/guide/cpp/basic-serialization.md
@@ -210,6 +210,63 @@ public:
};
```
+### Accessor Properties
+
+Use `FORY_PROPERTY` when the serialized field is exposed through accessor
+methods instead of a data member. This keeps the type registered as a normal
+struct type:
+
+```cpp
+struct AccountImpl {
+ int32_t id = 0;
+};
+
+class Account {
+public:
+ explicit Account(AccountImpl *impl) : impl_(impl) {}
+
+ const int32_t &id() const { return impl_->id; }
+ Account &id(int32_t value) {
+ impl_->id = value;
+ return *this;
+ }
+
+private:
+ AccountImpl *impl_ = nullptr;
+
+public:
+ FORY_STRUCT(Account, FORY_PROPERTY(id));
+};
+```
+
+`FORY_PROPERTY(id)` calls `obj.id()` to read the field and `obj.id(value)` to
+write it. The field type is inferred from the const getter return type with
+cv-qualifiers and references removed, so `const int32_t &` is treated as
+`int32_t`.
+
+Use the three-argument form when the getter and setter have different names:
+
+```cpp
+class User {
+public:
+ const int32_t &get_id() const;
+ void set_id(int32_t value);
+
+ FORY_STRUCT(User, FORY_PROPERTY(id, get_id, set_id));
+};
+```
+
+Field metadata can be attached as the final argument:
+
+```cpp
+FORY_STRUCT(Account, FORY_PROPERTY(id, fory::F().varint()));
+FORY_STRUCT(User, FORY_PROPERTY(id, get_id, set_id, fory::F(1).varint()));
+```
+
+When `FORY_STRUCT` is declared at namespace scope, the accessor methods must be
+public. For private PIMPL accessors or private data members, place
+`FORY_STRUCT` inside the class in a `public:` section.
+
The macro:
1. Generates compile-time field metadata
@@ -228,7 +285,7 @@ The macro:
## External / Third-Party Types
When you cannot modify a third-party type, use `FORY_STRUCT` at namespace
-scope. This only works with **public** fields.
+scope. This only works with public data members or public accessor methods.
```cpp
namespace thirdparty {
@@ -244,7 +301,7 @@ FORY_STRUCT(Foo, id, name);
**Limitations:**
- Must be declared at namespace scope in the same namespace as the type
-- Only public fields are supported
+- Only public data members or accessor methods are supported
## Inherited Fields
diff --git a/docs/guide/cpp/schema-metadata.md
b/docs/guide/cpp/schema-metadata.md
index 9d1c55542..fe455dd60 100644
--- a/docs/guide/cpp/schema-metadata.md
+++ b/docs/guide/cpp/schema-metadata.md
@@ -20,8 +20,8 @@ license: |
---
Field configuration is embedded directly in `FORY_STRUCT`. A field entry may be
-bare, or it may be a tuple containing the member name and a `fory::F(...)`
-builder:
+bare, it may be a tuple containing the member name and a `fory::F(...)`
+builder, or it may be a configured accessor property:
```cpp
#include "fory/serialization/fory.h"
@@ -35,6 +35,18 @@ struct DataV2 {
FORY_STRUCT(DataV2, id, (timestamp, fory::F().tagged()), version);
```
+Accessor properties use the same field metadata as data members:
+
+```cpp
+class Counter {
+public:
+ const uint32_t &value() const;
+ Counter &value(uint32_t value);
+
+ FORY_STRUCT(Counter, FORY_PROPERTY(value, fory::F().varint()));
+};
+```
+
The configuration is compile-time metadata. It does not allocate codec objects
or add virtual dispatch on the serialization path.
@@ -44,6 +56,7 @@ or add virtual dispatch on the serialization path.
```cpp
FORY_STRUCT(DataV2, id, (timestamp, fory::F().tagged()), version);
+FORY_STRUCT(Counter, FORY_PROPERTY(value, fory::F().varint()));
```
`fory::F(id)` uses explicit id-based field identity. IDs must be
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]