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 fe0285039 feat(c++): add cpp tuple serializer (#2975)
fe0285039 is described below

commit fe0285039d0fd4b5b031e41b533734642e4134ee
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Dec 3 19:48:04 2025 +0800

    feat(c++): add cpp tuple serializer (#2975)
    
    <!--
    **Thanks for contributing to Apache Fory™.**
    
    **If this is your first time opening a PR on fory, you can refer to
    
[CONTRIBUTING.md](https://github.com/apache/fory/blob/main/CONTRIBUTING.md).**
    
    Contribution Checklist
    
    - The **Apache Fory™** community has requirements on the naming of pr
    titles. You can also find instructions in
    [CONTRIBUTING.md](https://github.com/apache/fory/blob/main/CONTRIBUTING.md).
    
    - Apache Fory™ has a strong focus on performance. If the PR you submit
    will have an impact on performance, please benchmark it first and
    provide the benchmark result here.
    -->
    
    ## Why?
    
    <!-- Describe the purpose of this PR. -->
    
    ## What does this PR do?
    
    add cpp tuple serializer
    
    ## Related issues
    
    Closes #2973
    
    ## Does this PR introduce any user-facing change?
    
    <!--
    If any user-facing interface changes, please [open an
    issue](https://github.com/apache/fory/issues/new/choose) describing the
    need to do so and update the document if necessary.
    
    Delete section if not applicable.
    -->
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
    
    <!--
    When the PR has an impact on performance (if you don't know whether the
    PR will have an impact on performance, you can submit the PR first, and
    if it will have impact on performance, the code reviewer will explain
    it), be sure to attach a benchmark data here.
    
    Delete section if not applicable.
    -->
---
 cpp/fory/serialization/BUILD                    |  11 +
 cpp/fory/serialization/fory.h                   |   1 +
 cpp/fory/serialization/serializer_traits.h      |  11 +
 cpp/fory/serialization/tuple_serializer.h       | 468 ++++++++++++++++++++++++
 cpp/fory/serialization/tuple_serializer_test.cc | 290 +++++++++++++++
 cpp/fory/serialization/type_resolver.h          |  28 +-
 6 files changed, 808 insertions(+), 1 deletion(-)

diff --git a/cpp/fory/serialization/BUILD b/cpp/fory/serialization/BUILD
index 417db78c4..74e26fadc 100644
--- a/cpp/fory/serialization/BUILD
+++ b/cpp/fory/serialization/BUILD
@@ -23,6 +23,7 @@ cc_library(
         "smart_ptr_serializers.h",
         "struct_serializer.h",
         "temporal_serializers.h",
+        "tuple_serializer.h",
         "type_info.h",
         "type_resolver.h",
     ],
@@ -97,6 +98,16 @@ cc_test(
     ],
 )
 
+cc_test(
+    name = "tuple_serializer_test",
+    srcs = ["tuple_serializer_test.cc"],
+    deps = [
+        ":fory_serialization",
+        "@googletest//:gtest",
+        "@googletest//:gtest_main",
+    ],
+)
+
 cc_binary(
     name = "xlang_test_main",
     srcs = ["xlang_test_main.cc"],
diff --git a/cpp/fory/serialization/fory.h b/cpp/fory/serialization/fory.h
index 5d3cb8e24..fd9e58aea 100644
--- a/cpp/fory/serialization/fory.h
+++ b/cpp/fory/serialization/fory.h
@@ -28,6 +28,7 @@
 #include "fory/serialization/smart_ptr_serializers.h"
 #include "fory/serialization/struct_serializer.h"
 #include "fory/serialization/temporal_serializers.h"
+#include "fory/serialization/tuple_serializer.h"
 #include "fory/serialization/type_resolver.h"
 #include "fory/util/buffer.h"
 #include "fory/util/error.h"
diff --git a/cpp/fory/serialization/serializer_traits.h 
b/cpp/fory/serialization/serializer_traits.h
index 7496d56e2..ccee4e1e1 100644
--- a/cpp/fory/serialization/serializer_traits.h
+++ b/cpp/fory/serialization/serializer_traits.h
@@ -85,6 +85,14 @@ template <typename T> struct is_optional<std::optional<T>> : 
std::true_type {};
 template <typename T>
 inline constexpr bool is_optional_v = is_optional<T>::value;
 
+/// Detect std::tuple
+template <typename T> struct is_tuple : std::false_type {};
+
+template <typename... Ts>
+struct is_tuple<std::tuple<Ts...>> : std::true_type {};
+
+template <typename T> inline constexpr bool is_tuple_v = is_tuple<T>::value;
+
 /// Detect std::weak_ptr
 template <typename T> struct is_weak_ptr : std::false_type {};
 
@@ -224,6 +232,9 @@ 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... Ts>
+struct is_generic_type<std::tuple<Ts...>> : std::true_type {};
+
 template <typename T>
 inline constexpr bool is_generic_type_v = is_generic_type<T>::value;
 
diff --git a/cpp/fory/serialization/tuple_serializer.h 
b/cpp/fory/serialization/tuple_serializer.h
new file mode 100644
index 000000000..6b68a7cfa
--- /dev/null
+++ b/cpp/fory/serialization/tuple_serializer.h
@@ -0,0 +1,468 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include "fory/serialization/collection_serializer.h"
+#include "fory/serialization/serializer.h"
+#include <cstdint>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+
+namespace fory {
+namespace serialization {
+
+// ============================================================================
+// Compile-time Tuple Homogeneity Detection
+// ============================================================================
+
+/// Check if all types in a tuple are the same type at compile time.
+/// Polymorphic types are always treated as non-homogeneous.
+template <typename... Ts> struct is_tuple_homogeneous : std::false_type {};
+
+template <> struct is_tuple_homogeneous<> : std::true_type {};
+
+template <typename T> struct is_tuple_homogeneous<T> : std::true_type {};
+
+template <typename T, typename U, typename... Rest>
+struct is_tuple_homogeneous<T, U, Rest...>
+    : std::conditional_t<std::is_same_v<T, U> && !is_polymorphic_v<T> &&
+                             !is_shared_ref_v<T>,
+                         is_tuple_homogeneous<T, Rest...>, std::false_type> {};
+
+template <typename... Ts>
+inline constexpr bool is_tuple_homogeneous_v =
+    is_tuple_homogeneous<Ts...>::value;
+
+/// Get the first type of a tuple (for homogeneous tuple element type)
+template <typename Tuple> struct tuple_first_type;
+
+template <typename T, typename... Rest>
+struct tuple_first_type<std::tuple<T, Rest...>> {
+  using type = T;
+};
+
+template <typename Tuple>
+using tuple_first_type_t = typename tuple_first_type<Tuple>::type;
+
+// ============================================================================
+// Tuple Serialization Helpers
+// ============================================================================
+
+/// Write tuple elements directly (non-xlang mode)
+template <typename Tuple, size_t... Is>
+inline Result<void, Error>
+write_tuple_elements_direct(const Tuple &tuple, WriteContext &ctx,
+                            std::index_sequence<Is...>) {
+  Result<void, Error> result;
+  // Use fold expression to write each element
+  ((result = [&]() -> Result<void, Error> {
+     if (!result.ok())
+       return result;
+     using ElemType = std::tuple_element_t<Is, Tuple>;
+     const auto &elem = std::get<Is>(tuple);
+     // For nullable/shared types, use full write with ref tracking
+     if constexpr (is_nullable_v<ElemType> || is_shared_ref_v<ElemType> ||
+                   is_polymorphic_v<ElemType>) {
+       return Serializer<ElemType>::write(elem, ctx, true, false, false);
+     } else {
+       return Serializer<ElemType>::write_data(elem, ctx);
+     }
+   }()),
+   ...);
+  return result;
+}
+
+/// Write tuple elements with type info (xlang/compatible mode, heterogeneous)
+template <typename Tuple, size_t... Is>
+inline Result<void, Error>
+write_tuple_elements_heterogeneous(const Tuple &tuple, WriteContext &ctx,
+                                   std::index_sequence<Is...>) {
+  Result<void, Error> result;
+  // Write each element with its type info
+  ((result = [&]() -> Result<void, Error> {
+     if (!result.ok())
+       return result;
+     using ElemType = std::tuple_element_t<Is, Tuple>;
+     return Serializer<ElemType>::write(std::get<Is>(tuple), ctx, true, true,
+                                        false);
+   }()),
+   ...);
+  return result;
+}
+
+/// Write tuple elements without type info (xlang/compatible mode, homogeneous)
+template <typename Tuple, size_t... Is>
+inline Result<void, Error>
+write_tuple_elements_homogeneous(const Tuple &tuple, WriteContext &ctx,
+                                 std::index_sequence<Is...>) {
+  Result<void, Error> result;
+  // Write each element data only (type info written once in header)
+  ((result = [&]() -> Result<void, Error> {
+     if (!result.ok())
+       return result;
+     using ElemType = std::tuple_element_t<Is, Tuple>;
+     return Serializer<ElemType>::write_data(std::get<Is>(tuple), ctx);
+   }()),
+   ...);
+  return result;
+}
+
+/// Read tuple elements directly (non-xlang mode)
+template <typename Tuple, size_t... Is>
+inline Result<Tuple, Error>
+read_tuple_elements_direct(ReadContext &ctx, std::index_sequence<Is...>) {
+  Tuple result;
+  Error error;
+  bool has_error = false;
+
+  // Use fold expression to read each element
+  ((has_error ? void() : [&]() {
+      using ElemType = std::tuple_element_t<Is, Tuple>;
+      // For nullable/shared types, use full read with ref tracking
+      if constexpr (is_nullable_v<ElemType> || is_shared_ref_v<ElemType> ||
+                    is_polymorphic_v<ElemType>) {
+        auto elem_result = Serializer<ElemType>::read(ctx, true, false);
+        if (elem_result.ok()) {
+          std::get<Is>(result) = std::move(elem_result).value();
+        } else {
+          error = std::move(elem_result).error();
+          has_error = true;
+        }
+      } else {
+        auto elem_result = Serializer<ElemType>::read_data(ctx);
+        if (elem_result.ok()) {
+          std::get<Is>(result) = std::move(elem_result).value();
+        } else {
+          error = std::move(elem_result).error();
+          has_error = true;
+        }
+      }
+    }()),
+    ...);
+
+  if (has_error) {
+    return Unexpected(std::move(error));
+  }
+  return result;
+}
+
+/// Read tuple elements with type info (xlang/compatible mode, heterogeneous)
+template <typename Tuple, size_t... Is>
+inline Result<Tuple, Error>
+read_tuple_elements_heterogeneous(ReadContext &ctx, uint32_t length,
+                                  std::index_sequence<Is...>) {
+  Tuple result;
+  Error error;
+  bool has_error = false;
+  uint32_t index = 0;
+
+  // Read each element with its type info, handling length mismatch
+  ((has_error ? void() : [&]() {
+      using ElemType = std::tuple_element_t<Is, Tuple>;
+      if (index < length) {
+        auto elem_result = Serializer<ElemType>::read(ctx, true, true);
+        if (elem_result.ok()) {
+          std::get<Is>(result) = std::move(elem_result).value();
+        } else {
+          error = std::move(elem_result).error();
+          has_error = true;
+        }
+        ++index;
+      }
+      // If index >= length, use default-constructed value
+    }()),
+    ...);
+
+  if (has_error) {
+    return Unexpected(std::move(error));
+  }
+
+  // Skip any extra elements beyond tuple size
+  while (index < length) {
+    // Skip value - read type and skip data
+    FORY_TRY(type_info, ctx.read_any_typeinfo());
+    // For simplicity, read and discard - in practice would need skip logic
+    (void)type_info;
+    ++index;
+  }
+
+  return result;
+}
+
+/// Read tuple elements without type info (xlang/compatible mode, homogeneous)
+template <typename Tuple, size_t... Is>
+inline Result<Tuple, Error>
+read_tuple_elements_homogeneous(ReadContext &ctx, uint32_t length,
+                                std::index_sequence<Is...>) {
+  Tuple result;
+  Error error;
+  bool has_error = false;
+  uint32_t index = 0;
+
+  // Read each element data only (type info read once in header)
+  ((has_error ? void() : [&]() {
+      using ElemType = std::tuple_element_t<Is, Tuple>;
+      if (index < length) {
+        auto elem_result = Serializer<ElemType>::read_data(ctx);
+        if (elem_result.ok()) {
+          std::get<Is>(result) = std::move(elem_result).value();
+        } else {
+          error = std::move(elem_result).error();
+          has_error = true;
+        }
+        ++index;
+      }
+      // If index >= length, use default-constructed value
+    }()),
+    ...);
+
+  if (has_error) {
+    return Unexpected(std::move(error));
+  }
+
+  // Skip any extra elements beyond tuple size
+  using ElemType = tuple_first_type_t<Tuple>;
+  while (index < length) {
+    auto skip_result = Serializer<ElemType>::read_data(ctx);
+    if (!skip_result.ok()) {
+      return Unexpected(std::move(skip_result).error());
+    }
+    ++index;
+  }
+
+  return result;
+}
+
+// ============================================================================
+// std::tuple Serializer
+// ============================================================================
+
+/// Empty tuple serializer
+template <> struct Serializer<std::tuple<>> {
+  static constexpr TypeId type_id = TypeId::LIST;
+
+  static inline Result<void, Error> write_type_info(WriteContext &ctx) {
+    ctx.write_varuint32(static_cast<uint32_t>(type_id));
+    return {};
+  }
+
+  static inline Result<void, Error> read_type_info(ReadContext &ctx) {
+    FORY_TRY(type_info, ctx.read_any_typeinfo());
+    if (!type_id_matches(type_info->type_id, static_cast<uint32_t>(type_id))) {
+      return Unexpected(Error::type_mismatch(type_info->type_id,
+                                             static_cast<uint32_t>(type_id)));
+    }
+    return {};
+  }
+
+  static inline Result<void, Error> write(const std::tuple<> &,
+                                          WriteContext &ctx, bool write_ref,
+                                          bool write_type,
+                                          bool has_generics = false) {
+    (void)has_generics;
+    write_not_null_ref_flag(ctx, write_ref);
+    if (write_type) {
+      ctx.write_varuint32(static_cast<uint32_t>(type_id));
+    }
+    return write_data(std::tuple<>(), ctx);
+  }
+
+  static inline Result<void, Error> write_data(const std::tuple<> &,
+                                               WriteContext &ctx) {
+    ctx.write_varuint32(0); // length = 0
+    return {};
+  }
+
+  static inline Result<void, Error>
+  write_data_generic(const std::tuple<> &tuple, WriteContext &ctx,
+                     bool has_generics) {
+    (void)has_generics;
+    return write_data(tuple, ctx);
+  }
+
+  static inline Result<std::tuple<>, Error>
+  read(ReadContext &ctx, bool read_ref, bool read_type) {
+    FORY_TRY(has_value, consume_ref_flag(ctx, read_ref));
+    if (!has_value) {
+      return std::tuple<>();
+    }
+
+    Error error;
+    if (read_type) {
+      uint32_t type_id_read = ctx.read_varuint32(&error);
+      if (FORY_PREDICT_FALSE(!error.ok())) {
+        return Unexpected(std::move(error));
+      }
+      if (!type_id_matches(type_id_read, static_cast<uint32_t>(type_id))) {
+        return Unexpected(
+            Error::type_mismatch(type_id_read, 
static_cast<uint32_t>(type_id)));
+      }
+    }
+    return read_data(ctx);
+  }
+
+  static inline Result<std::tuple<>, Error> read_data(ReadContext &ctx) {
+    Error error;
+    uint32_t length = ctx.read_varuint32(&error);
+    if (FORY_PREDICT_FALSE(!error.ok())) {
+      return Unexpected(std::move(error));
+    }
+    (void)length; // Ignore - empty tuple
+    return std::tuple<>();
+  }
+};
+
+/// Generic tuple serializer for tuples with 1+ elements
+template <typename... Ts> struct Serializer<std::tuple<Ts...>> {
+  static constexpr TypeId type_id = TypeId::LIST;
+  static constexpr size_t tuple_size = sizeof...(Ts);
+  static constexpr bool is_homogeneous = is_tuple_homogeneous_v<Ts...>;
+
+  using TupleType = std::tuple<Ts...>;
+  using IndexSeq = std::index_sequence_for<Ts...>;
+
+  static inline Result<void, Error> write_type_info(WriteContext &ctx) {
+    ctx.write_varuint32(static_cast<uint32_t>(type_id));
+    return {};
+  }
+
+  static inline Result<void, Error> read_type_info(ReadContext &ctx) {
+    FORY_TRY(type_info, ctx.read_any_typeinfo());
+    if (!type_id_matches(type_info->type_id, static_cast<uint32_t>(type_id))) {
+      return Unexpected(Error::type_mismatch(type_info->type_id,
+                                             static_cast<uint32_t>(type_id)));
+    }
+    return {};
+  }
+
+  static inline Result<void, Error> write(const TupleType &tuple,
+                                          WriteContext &ctx, bool write_ref,
+                                          bool write_type,
+                                          bool has_generics = false) {
+    write_not_null_ref_flag(ctx, write_ref);
+    if (write_type) {
+      ctx.write_varuint32(static_cast<uint32_t>(type_id));
+    }
+    return write_data_generic(tuple, ctx, has_generics);
+  }
+
+  static inline Result<void, Error> write_data(const TupleType &tuple,
+                                               WriteContext &ctx) {
+    if (!ctx.is_compatible() && !ctx.is_xlang()) {
+      // Non-compatible mode: write elements directly without collection header
+      return write_tuple_elements_direct(tuple, ctx, IndexSeq{});
+    }
+
+    // xlang/compatible mode: use collection protocol
+
+    // Write length
+    ctx.write_varuint32(static_cast<uint32_t>(tuple_size));
+
+    if constexpr (tuple_size == 0) {
+      return {};
+    }
+
+    // Build header bitmap - always heterogeneous for tuples in xlang mode
+    // (following Rust's approach for simplicity and cross-language compat)
+    uint8_t bitmap = 0;
+    ctx.write_uint8(bitmap);
+
+    // Write elements with type info per element
+    return write_tuple_elements_heterogeneous(tuple, ctx, IndexSeq{});
+  }
+
+  static inline Result<void, Error> write_data_generic(const TupleType &tuple,
+                                                       WriteContext &ctx,
+                                                       bool has_generics) {
+    (void)has_generics;
+    return write_data(tuple, ctx);
+  }
+
+  static inline Result<TupleType, Error> read(ReadContext &ctx, bool read_ref,
+                                              bool read_type) {
+    FORY_TRY(has_value, consume_ref_flag(ctx, read_ref));
+    if (!has_value) {
+      return TupleType{};
+    }
+
+    Error error;
+    if (read_type) {
+      uint32_t type_id_read = ctx.read_varuint32(&error);
+      if (FORY_PREDICT_FALSE(!error.ok())) {
+        return Unexpected(std::move(error));
+      }
+      if (!type_id_matches(type_id_read, static_cast<uint32_t>(type_id))) {
+        return Unexpected(
+            Error::type_mismatch(type_id_read, 
static_cast<uint32_t>(type_id)));
+      }
+    }
+
+    return read_data(ctx);
+  }
+
+  static inline Result<TupleType, Error> read_data(ReadContext &ctx) {
+    if (!ctx.is_compatible() && !ctx.is_xlang()) {
+      // Non-compatible mode: read elements directly
+      return read_tuple_elements_direct<TupleType>(ctx, IndexSeq{});
+    }
+
+    // xlang/compatible mode: read collection protocol
+    Error error;
+    uint32_t length = ctx.read_varuint32(&error);
+    if (FORY_PREDICT_FALSE(!error.ok())) {
+      return Unexpected(std::move(error));
+    }
+
+    if (length == 0) {
+      return TupleType{};
+    }
+
+    // Read header bitmap
+    uint8_t bitmap = ctx.read_uint8(&error);
+    if (FORY_PREDICT_FALSE(!error.ok())) {
+      return Unexpected(std::move(error));
+    }
+
+    bool is_same_type = (bitmap & COLL_IS_SAME_TYPE) != 0;
+
+    if (is_same_type) {
+      // Read element type info once
+      FORY_TRY(elem_type_info, ctx.read_any_typeinfo());
+      (void)elem_type_info;
+
+      return read_tuple_elements_homogeneous<TupleType>(ctx, length,
+                                                        IndexSeq{});
+    } else {
+      return read_tuple_elements_heterogeneous<TupleType>(ctx, length,
+                                                          IndexSeq{});
+    }
+  }
+
+  static inline Result<TupleType, Error>
+  read_with_type_info(ReadContext &ctx, bool read_ref,
+                      const TypeInfo &type_info) {
+    (void)type_info;
+    return read(ctx, read_ref, false);
+  }
+};
+
+} // namespace serialization
+} // namespace fory
diff --git a/cpp/fory/serialization/tuple_serializer_test.cc 
b/cpp/fory/serialization/tuple_serializer_test.cc
new file mode 100644
index 000000000..64c3b19b0
--- /dev/null
+++ b/cpp/fory/serialization/tuple_serializer_test.cc
@@ -0,0 +1,290 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "fory/serialization/fory.h"
+#include "gtest/gtest.h"
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <tuple>
+
+namespace fory {
+namespace serialization {
+namespace {
+
+// ============================================================================
+// Compile-time Homogeneity Detection Tests
+// ============================================================================
+
+TEST(TupleSerializerTest, HomogeneityDetection) {
+  // Empty tuple is homogeneous
+  static_assert(is_tuple_homogeneous_v<>, "Empty should be homogeneous");
+
+  // Single element is homogeneous
+  static_assert(is_tuple_homogeneous_v<int>, "Single int is homogeneous");
+  static_assert(is_tuple_homogeneous_v<std::string>,
+                "Single string is homogeneous");
+
+  // Same types are homogeneous
+  static_assert(is_tuple_homogeneous_v<int, int>, "int,int is homogeneous");
+  static_assert(is_tuple_homogeneous_v<int, int, int>,
+                "int,int,int is homogeneous");
+  static_assert(is_tuple_homogeneous_v<std::string, std::string>,
+                "string,string is homogeneous");
+
+  // Different types are heterogeneous
+  static_assert(!is_tuple_homogeneous_v<int, double>,
+                "int,double is heterogeneous");
+  static_assert(!is_tuple_homogeneous_v<int, std::string>,
+                "int,string is heterogeneous");
+  static_assert(!is_tuple_homogeneous_v<int, int, double>,
+                "int,int,double is heterogeneous");
+}
+
+// ============================================================================
+// Holder Structs for Testing
+// ============================================================================
+
+// First test with vector to verify test setup
+struct VectorHolder {
+  std::vector<int32_t> values;
+};
+FORY_STRUCT(VectorHolder, values);
+
+struct TupleHomogeneousHolder {
+  std::tuple<int32_t, int32_t, int32_t> values;
+};
+FORY_STRUCT(TupleHomogeneousHolder, values);
+
+struct TupleHeterogeneousHolder {
+  std::tuple<int32_t, std::string, double> values;
+};
+FORY_STRUCT(TupleHeterogeneousHolder, values);
+
+struct TupleSingleHolder {
+  std::tuple<std::string> value;
+};
+FORY_STRUCT(TupleSingleHolder, value);
+
+struct TupleEmptyHolder {
+  std::tuple<> value;
+};
+FORY_STRUCT(TupleEmptyHolder, value);
+
+struct TupleNestedHolder {
+  std::tuple<std::tuple<int32_t, int32_t>, std::string> values;
+};
+FORY_STRUCT(TupleNestedHolder, values);
+
+Fory create_fory() {
+  return Fory::builder().xlang(true).track_ref(true).build();
+}
+
+// ============================================================================
+// Round-Trip Tests
+// ============================================================================
+
+// Verify test setup works with vector first
+TEST(TupleSerializerTest, VectorRoundTrip) {
+  auto fory = create_fory();
+  fory.register_struct<VectorHolder>(299);
+
+  VectorHolder original;
+  original.values = {10, 20, 30};
+
+  auto bytes_result = fory.serialize(original);
+  ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+  auto deserialize_result = fory.deserialize<VectorHolder>(
+      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.values.size(), 3u);
+  EXPECT_EQ(deserialized.values[0], 10);
+  EXPECT_EQ(deserialized.values[1], 20);
+  EXPECT_EQ(deserialized.values[2], 30);
+}
+
+TEST(TupleSerializerTest, HomogeneousTupleRoundTrip) {
+  auto fory = create_fory();
+  auto reg_result = fory.register_struct<TupleHomogeneousHolder>(300);
+  ASSERT_TRUE(reg_result.ok())
+      << "Registration failed: " << reg_result.error().to_string();
+
+  TupleHomogeneousHolder original;
+  original.values = std::make_tuple(10, 20, 30);
+
+  auto bytes_result = fory.serialize(original);
+  ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+  auto deserialize_result = fory.deserialize<TupleHomogeneousHolder>(
+      bytes_result->data(), bytes_result->size());
+  ASSERT_TRUE(deserialize_result.ok())
+      << deserialize_result.error().to_string();
+
+  auto deserialized = std::move(deserialize_result).value();
+  EXPECT_EQ(std::get<0>(deserialized.values), 10);
+  EXPECT_EQ(std::get<1>(deserialized.values), 20);
+  EXPECT_EQ(std::get<2>(deserialized.values), 30);
+}
+
+TEST(TupleSerializerTest, HeterogeneousTupleRoundTrip) {
+  auto fory = create_fory();
+  fory.register_struct<TupleHeterogeneousHolder>(301);
+
+  TupleHeterogeneousHolder original;
+  original.values = std::make_tuple(42, std::string("hello"), 3.14);
+
+  auto bytes_result = fory.serialize(original);
+  ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+  auto deserialize_result = fory.deserialize<TupleHeterogeneousHolder>(
+      bytes_result->data(), bytes_result->size());
+  ASSERT_TRUE(deserialize_result.ok())
+      << deserialize_result.error().to_string();
+
+  auto deserialized = std::move(deserialize_result).value();
+  EXPECT_EQ(std::get<0>(deserialized.values), 42);
+  EXPECT_EQ(std::get<1>(deserialized.values), "hello");
+  EXPECT_DOUBLE_EQ(std::get<2>(deserialized.values), 3.14);
+}
+
+TEST(TupleSerializerTest, SingleElementTupleRoundTrip) {
+  auto fory = create_fory();
+  fory.register_struct<TupleSingleHolder>(302);
+
+  TupleSingleHolder original;
+  original.value = std::make_tuple(std::string("single"));
+
+  auto bytes_result = fory.serialize(original);
+  ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+  auto deserialize_result = fory.deserialize<TupleSingleHolder>(
+      bytes_result->data(), bytes_result->size());
+  ASSERT_TRUE(deserialize_result.ok())
+      << deserialize_result.error().to_string();
+
+  auto deserialized = std::move(deserialize_result).value();
+  EXPECT_EQ(std::get<0>(deserialized.value), "single");
+}
+
+TEST(TupleSerializerTest, EmptyTupleRoundTrip) {
+  auto fory = create_fory();
+  fory.register_struct<TupleEmptyHolder>(303);
+
+  TupleEmptyHolder original;
+
+  auto bytes_result = fory.serialize(original);
+  ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+  auto deserialize_result = fory.deserialize<TupleEmptyHolder>(
+      bytes_result->data(), bytes_result->size());
+  ASSERT_TRUE(deserialize_result.ok())
+      << deserialize_result.error().to_string();
+}
+
+TEST(TupleSerializerTest, NestedTupleRoundTrip) {
+  auto fory = create_fory();
+  fory.register_struct<TupleNestedHolder>(304);
+
+  TupleNestedHolder original;
+  original.values =
+      std::make_tuple(std::make_tuple(1, 2), std::string("nested"));
+
+  auto bytes_result = fory.serialize(original);
+  ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+  auto deserialize_result = fory.deserialize<TupleNestedHolder>(
+      bytes_result->data(), bytes_result->size());
+  ASSERT_TRUE(deserialize_result.ok())
+      << deserialize_result.error().to_string();
+
+  auto deserialized = std::move(deserialize_result).value();
+  auto inner = std::get<0>(deserialized.values);
+  EXPECT_EQ(std::get<0>(inner), 1);
+  EXPECT_EQ(std::get<1>(inner), 2);
+  EXPECT_EQ(std::get<1>(deserialized.values), "nested");
+}
+
+// ============================================================================
+// Large Tuple Test (testing limit of implementation)
+// ============================================================================
+
+struct TupleLargeHolder {
+  std::tuple<int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t,
+             int32_t, int32_t, int32_t>
+      values;
+};
+FORY_STRUCT(TupleLargeHolder, values);
+
+TEST(TupleSerializerTest, LargeTupleRoundTrip) {
+  auto fory = create_fory();
+  fory.register_struct<TupleLargeHolder>(305);
+
+  TupleLargeHolder original;
+  original.values = std::make_tuple(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
+
+  auto bytes_result = fory.serialize(original);
+  ASSERT_TRUE(bytes_result.ok()) << bytes_result.error().to_string();
+
+  auto deserialize_result = fory.deserialize<TupleLargeHolder>(
+      bytes_result->data(), bytes_result->size());
+  ASSERT_TRUE(deserialize_result.ok())
+      << deserialize_result.error().to_string();
+
+  auto deserialized = std::move(deserialize_result).value();
+  EXPECT_EQ(std::get<0>(deserialized.values), 1);
+  EXPECT_EQ(std::get<4>(deserialized.values), 5);
+  EXPECT_EQ(std::get<9>(deserialized.values), 10);
+}
+
+// ============================================================================
+// Homogeneous Optimization Verification
+// ============================================================================
+
+TEST(TupleSerializerTest, HomogeneousOptimizationSize) {
+  auto fory = create_fory();
+  fory.register_struct<TupleHomogeneousHolder>(306);
+  fory.register_struct<TupleHeterogeneousHolder>(307);
+
+  // Homogeneous tuple: type info written once
+  TupleHomogeneousHolder homo;
+  homo.values = std::make_tuple(1, 2, 3);
+
+  auto homo_bytes = fory.serialize(homo);
+  ASSERT_TRUE(homo_bytes.ok());
+
+  // Heterogeneous tuple: type info written per element
+  TupleHeterogeneousHolder hetero;
+  hetero.values = std::make_tuple(1, std::string("x"), 1.0);
+
+  auto hetero_bytes = fory.serialize(hetero);
+  ASSERT_TRUE(hetero_bytes.ok());
+
+  // Both should serialize successfully - size comparison is informational
+  // Homogeneous should be smaller due to single type info
+  EXPECT_GT(homo_bytes->size(), 0u);
+  EXPECT_GT(hetero_bytes->size(), 0u);
+}
+
+} // namespace
+} // namespace serialization
+} // namespace fory
diff --git a/cpp/fory/serialization/type_resolver.h 
b/cpp/fory/serialization/type_resolver.h
index e8af52607..078a15c60 100644
--- a/cpp/fory/serialization/type_resolver.h
+++ b/cpp/fory/serialization/type_resolver.h
@@ -369,13 +369,39 @@ struct FieldTypeBuilder<std::basic_string_view<CharT, 
Traits>, void> {
   }
 };
 
+// Tuple FieldTypeBuilder - builds FieldType with all element types as generics
+template <typename T>
+struct FieldTypeBuilder<T, std::enable_if_t<is_tuple_v<decay_t<T>>>> {
+  using Tuple = decay_t<T>;
+
+  template <size_t... Is>
+  static void add_element_types(FieldType &ft, std::index_sequence<Is...>) {
+    (ft.generics.push_back(
+         FieldTypeBuilder<std::tuple_element_t<Is, Tuple>>::build(false)),
+     ...);
+  }
+
+  static FieldType build(bool nullable) {
+    constexpr size_t tuple_size = std::tuple_size_v<Tuple>;
+    FieldType ft(to_type_id(Serializer<Tuple>::type_id), nullable);
+    if constexpr (tuple_size > 0) {
+      add_element_types(ft, std::make_index_sequence<tuple_size>{});
+    } else {
+      // Empty tuple: use STRUCT as stub element type for schema encoding
+      ft.generics.push_back(
+          FieldType(static_cast<uint32_t>(TypeId::STRUCT), false));
+    }
+    return ft;
+  }
+};
+
 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_string_view_v<decay_t<T>> && !is_tuple_v<decay_t<T>> &&
            has_serializer_type_id_v<decay_t<T>>>> {
   using Decayed = decay_t<T>;
   static FieldType build(bool nullable) {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to