cpp: add template_streamop generation with runtime/compiler test coverage
Add template_streamop support in the C++ generator so generated operator<< and printTo can target generic stream-like output types.
Keep default behavior unchanged when the option is not set (std::ostream signatures remain).
Add compiler/runtime coverage for template generation, friend declaration correctness, enums, and collection printing.
default:
```cpp
std::ostream& operator<<(std::ostream& out, const SimpleStruct& obj);
class SimpleStruct {
public:
void printTo(std::ostream& out) const;
};
```
with `template_streamop`:
```cpp
template <typename OStream_>
OStream_& operator<<(OStream_& out, const SimpleStruct& obj);
class SimpleStruct {
public:
template <typename OStream_>
void printTo(OStream_& out) const;
};
```
diff --git a/test/cpp/CMakeLists.txt b/test/cpp/CMakeLists.txt
index 2403c87..6fdc95e 100644
--- a/test/cpp/CMakeLists.txt
+++ b/test/cpp/CMakeLists.txt
@@ -164,6 +164,29 @@
target_link_libraries(EnumClassTest thrift)
add_test(NAME EnumClassTest COMMAND EnumClassTest)
+# TemplateStreamOpTest - tests the template_streamop option
+set(templatestreamoptestgencpp_SOURCES
+ gen-cpp-templatestreamop/gen-cpp/ThriftTest_types.cpp
+ gen-cpp-templatestreamop/gen-cpp/ThriftTest_constants.cpp
+ src/ThriftTest_extras.cpp
+)
+add_library(templatestreamoptestgencpp STATIC ${templatestreamoptestgencpp_SOURCES})
+target_include_directories(templatestreamoptestgencpp BEFORE PRIVATE
+ "${CMAKE_CURRENT_BINARY_DIR}/gen-cpp-templatestreamop"
+ "${CMAKE_CURRENT_BINARY_DIR}"
+ "${PROJECT_SOURCE_DIR}/lib/cpp/src"
+)
+target_link_libraries(templatestreamoptestgencpp thrift)
+
+add_executable(TemplateStreamOpTest src/TemplateStreamOpTest.cpp)
+target_include_directories(TemplateStreamOpTest BEFORE PRIVATE
+ "${CMAKE_CURRENT_BINARY_DIR}/gen-cpp-templatestreamop/gen-cpp"
+ "${CMAKE_CURRENT_BINARY_DIR}/gen-cpp-templatestreamop"
+)
+target_link_libraries(TemplateStreamOpTest templatestreamoptestgencpp ${Boost_LIBRARIES})
+target_link_libraries(TemplateStreamOpTest thrift)
+add_test(NAME TemplateStreamOpTest COMMAND TemplateStreamOpTest)
+
#
# Common thrift code generation rules
#
@@ -193,6 +216,13 @@
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
+# Generate ThriftTest with template_streamop option for TemplateStreamOpTest
+add_custom_command(OUTPUT gen-cpp-templatestreamop/gen-cpp/ThriftTest_types.cpp gen-cpp-templatestreamop/gen-cpp/ThriftTest_types.h gen-cpp-templatestreamop/gen-cpp/ThriftTest_constants.cpp
+ COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/gen-cpp-templatestreamop
+ COMMAND ${THRIFT_COMPILER} --gen cpp:template_streamop -o gen-cpp-templatestreamop ${PROJECT_SOURCE_DIR}/test/ThriftTest.thrift
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+
add_custom_command(OUTPUT gen-cpp/Service.cpp
COMMAND ${THRIFT_COMPILER} --gen cpp ${PROJECT_SOURCE_DIR}/test/StressTest.thrift
)
diff --git a/test/cpp/Makefile.am b/test/cpp/Makefile.am
index f2d07ec..720a4b3 100644
--- a/test/cpp/Makefile.am
+++ b/test/cpp/Makefile.am
@@ -28,7 +28,10 @@
gen-cpp-private/ThriftTest_types.cpp \
gen-cpp-private/ThriftTest_constants.cpp \
gen-cpp-enumclass/ThriftTest_types.cpp \
- gen-cpp-enumclass/ThriftTest_constants.cpp
+ gen-cpp-enumclass/ThriftTest_constants.cpp \
+ gen-cpp-templatestreamop/ThriftTest_types.cpp \
+ gen-cpp-templatestreamop/ThriftTest_types.tcc \
+ gen-cpp-templatestreamop/ThriftTest_constants.cpp
noinst_LTLIBRARIES = libtestgencpp.la libstresstestgencpp.la
nodist_libtestgencpp_la_SOURCES = \
@@ -51,7 +54,8 @@
noinst_LTLIBRARIES += \
libforwardsettertestgencpp.la \
libprivateoptonaltestgencpp.la \
- libenumclasstestgencpp.la
+ libenumclasstestgencpp.la \
+ libtemplatestreamoptestgencpp.la
nodist_libforwardsettertestgencpp_la_SOURCES = \
gen-cpp-forward/ThriftTest_types.cpp \
@@ -81,6 +85,16 @@
libenumclasstestgencpp_la_LIBADD = $(top_builddir)/lib/cpp/libthrift.la
+nodist_libtemplatestreamoptestgencpp_la_SOURCES = \
+ gen-cpp-templatestreamop/ThriftTest_types.cpp \
+ gen-cpp-templatestreamop/ThriftTest_types.h \
+ gen-cpp-templatestreamop/ThriftTest_types.tcc \
+ gen-cpp-templatestreamop/ThriftTest_constants.cpp \
+ gen-cpp-templatestreamop/ThriftTest_constants.h \
+ src/ThriftTest_extras.cpp
+
+libtemplatestreamoptestgencpp_la_LIBADD = $(top_builddir)/lib/cpp/libthrift.la
+
nodist_libstresstestgencpp_la_SOURCES = \
gen-cpp/StressTest_types.h \
gen-cpp/Service.cpp \
@@ -97,7 +111,8 @@
StressTestNonBlocking \
ForwardSetterTest \
PrivateOptionalTest \
- EnumClassTest
+ EnumClassTest \
+ TemplateStreamOpTest
# we currently do not run the testsuite, stop c++ server issue
# TESTS = \
@@ -162,6 +177,14 @@
libenumclasstestgencpp.la \
$(top_builddir)/lib/cpp/libthrift.la
+TemplateStreamOpTest_SOURCES = \
+ src/TemplateStreamOpTest.cpp
+
+TemplateStreamOpTest_CPPFLAGS = -Igen-cpp-templatestreamop $(AM_CPPFLAGS)
+TemplateStreamOpTest_LDADD = \
+ libtemplatestreamoptestgencpp.la \
+ $(top_builddir)/lib/cpp/libthrift.la
+
#
# Common thrift code generation rules
#
@@ -183,6 +206,11 @@
$(MKDIR_P) gen-cpp-enumclass
$(THRIFT) --gen cpp:pure_enums=enum_class -out gen-cpp-enumclass $<
+# Generate ThriftTest with template_streamop option
+gen-cpp-templatestreamop/ThriftTest_types.cpp gen-cpp-templatestreamop/ThriftTest_types.h gen-cpp-templatestreamop/ThriftTest_types.tcc gen-cpp-templatestreamop/ThriftTest_constants.cpp: $(top_srcdir)/test/ThriftTest.thrift $(THRIFT)
+ $(MKDIR_P) gen-cpp-templatestreamop
+ $(THRIFT) --gen cpp:template_streamop -out gen-cpp-templatestreamop $<
+
gen-cpp/Service.cpp: $(top_srcdir)/test/StressTest.thrift $(THRIFT)
$(THRIFT) --gen cpp $<
@@ -194,7 +222,7 @@
AM_LDFLAGS = $(BOOST_LDFLAGS) $(LIBEVENT_LDFLAGS) $(ZLIB_LIBS)
clean-local:
- $(RM) -r gen-cpp/ gen-cpp-forward/ gen-cpp-private/ gen-cpp-enumclass/
+ $(RM) -r gen-cpp/ gen-cpp-forward/ gen-cpp-private/ gen-cpp-enumclass/ gen-cpp-templatestreamop/
style-local:
$(CPPSTYLE_CMD)
@@ -209,4 +237,5 @@
src/StressTestNonBlocking.cpp \
src/ForwardSetterTest.cpp \
src/PrivateOptionalTest.cpp \
- src/EnumClassTest.cpp
+ src/EnumClassTest.cpp \
+ src/TemplateStreamOpTest.cpp
diff --git a/test/cpp/src/EnumClassTest.cpp b/test/cpp/src/EnumClassTest.cpp
index 3b21063..673d6fb 100644
--- a/test/cpp/src/EnumClassTest.cpp
+++ b/test/cpp/src/EnumClassTest.cpp
@@ -23,8 +23,10 @@
*/
#include <iostream>
+#include <sstream>
#include <cassert>
#include <type_traits>
+#include <string>
// Include generated thrift types with enum_class option
#include "ThriftTest_types.h"
@@ -86,7 +88,88 @@
std::cout << " ✓ Enum class in switch statements works" << std::endl;
}
+ // Test 5: Verify to_string() works with enum class
+ {
+ Numberz one = Numberz::ONE;
+ Numberz five = Numberz::FIVE;
+ Numberz eight = Numberz::EIGHT;
+
+ std::string str_one = to_string(one);
+ std::string str_five = to_string(five);
+ std::string str_eight = to_string(eight);
+
+ assert(str_one == "ONE");
+ assert(str_five == "FIVE");
+ assert(str_eight == "EIGHT");
+ std::cout << " ✓ to_string() with enum class works (ONE, FIVE, EIGHT)" << std::endl;
+ }
+
+ // Test 6: Verify operator<< works with enum class
+ {
+ Numberz two = Numberz::TWO;
+ Numberz three = Numberz::THREE;
+
+ std::ostringstream oss;
+ oss << two << " and " << three;
+
+ std::string result = oss.str();
+ assert(result == "TWO and THREE");
+ std::cout << " ✓ operator<< with enum class works (TWO and THREE)" << std::endl;
+ }
+
+ // Test 7: Verify to_string() for invalid/cast enum values
+ {
+ // Cast an invalid value to enum (edge case testing)
+ Numberz invalid = static_cast<Numberz>(999);
+ std::string str_invalid = to_string(invalid);
+
+ // Should fall back to numeric representation
+ assert(str_invalid == "999");
+ std::cout << " ✓ to_string() handles invalid enum values (999)" << std::endl;
+ }
+
+ // Test 8: Verify operator<< for invalid/cast enum values
+ {
+ Numberz invalid = static_cast<Numberz>(777);
+ std::ostringstream oss;
+ oss << invalid;
+
+ std::string result = oss.str();
+ assert(result == "777");
+ std::cout << " ✓ operator<< handles invalid enum values (777)" << std::endl;
+ }
+
+ // Test 9: Verify enum class with zero value
+ {
+ Numberz zero = static_cast<Numberz>(0);
+ std::string str_zero = to_string(zero);
+
+ std::ostringstream oss;
+ oss << zero;
+
+ // Both should output "0" since there's no named value
+ assert(str_zero == "0");
+ assert(oss.str() == "0");
+ std::cout << " ✓ to_string() and operator<< work with zero value" << std::endl;
+ }
+
+ // Test 10: Verify all Numberz enum values can be converted to string
+ {
+ std::ostringstream oss;
+ oss << Numberz::ONE << ", "
+ << Numberz::TWO << ", "
+ << Numberz::THREE << ", "
+ << Numberz::FIVE << ", "
+ << Numberz::SIX << ", "
+ << Numberz::EIGHT;
+
+ std::string result = oss.str();
+ assert(result == "ONE, TWO, THREE, FIVE, SIX, EIGHT");
+ std::cout << " ✓ All Numberz enum values stream correctly" << std::endl;
+ }
+
std::cout << "\n✅ All pure_enums=enum_class tests passed!" << std::endl;
std::cout << " Verified at compile-time: enum class properties enforced" << std::endl;
+ std::cout << " Verified at runtime: to_string(), operator<< work correctly" << std::endl;
return 0;
}
diff --git a/test/cpp/src/TemplateStreamOpTest.cpp b/test/cpp/src/TemplateStreamOpTest.cpp
new file mode 100644
index 0000000..d97288d
--- /dev/null
+++ b/test/cpp/src/TemplateStreamOpTest.cpp
@@ -0,0 +1,472 @@
+/*
+ * 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.
+ */
+
+/**
+ * Test file to verify that template_streamop generated code compiles and works correctly.
+ * This tests the templated operator<< and printTo with various stream types.
+ */
+
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <vector>
+#include <map>
+#include <cassert>
+#include <chrono>
+#include <cstring>
+#include <cstdio>
+#include <cstdint>
+
+// Include generated thrift types with template_streamop option
+#include "ThriftTest_types.h"
+#include <thrift/TToString.h>
+
+using namespace thrift::test;
+
+// Custom minimal stream implementation for testing and performance comparison
+class MinimalStream {
+private:
+ static constexpr size_t STACK_BUFFER_SIZE = 2048;
+ char stack_buffer_[STACK_BUFFER_SIZE];
+ char* buffer_;
+ size_t size_;
+ size_t capacity_;
+ bool on_heap_;
+
+ void ensure_capacity(size_t additional) {
+ size_t needed = size_ + additional;
+ if (needed <= capacity_) return;
+
+ size_t new_capacity = capacity_;
+ while (new_capacity < needed) {
+ new_capacity *= 2;
+ }
+
+ char* new_buffer = new char[new_capacity];
+ if (size_ > 0) {
+ std::memcpy(new_buffer, buffer_, size_);
+ }
+
+ if (on_heap_) {
+ delete[] buffer_;
+ }
+
+ buffer_ = new_buffer;
+ capacity_ = new_capacity;
+ on_heap_ = true;
+ }
+
+ void append(const char* s, size_t len) {
+ ensure_capacity(len);
+ std::memcpy(buffer_ + size_, s, len);
+ size_ += len;
+ }
+
+ // Helper to print integer directly to buffer
+ template<typename T>
+ void print_integer(T value) {
+ char temp[32]; // Enough for any 64-bit integer
+ char* p = temp + sizeof(temp);
+ bool negative = value < 0;
+
+ if (negative) {
+ value = -value;
+ }
+
+ do {
+ *--p = '0' + (value % 10);
+ value /= 10;
+ } while (value > 0);
+
+ if (negative) {
+ *--p = '-';
+ }
+
+ append(p, temp + sizeof(temp) - p);
+ }
+
+ // Helper to print unsigned integer directly to buffer
+ template<typename T>
+ void print_unsigned(T value) {
+ char temp[32];
+ char* p = temp + sizeof(temp);
+
+ do {
+ *--p = '0' + (value % 10);
+ value /= 10;
+ } while (value > 0);
+
+ append(p, temp + sizeof(temp) - p);
+ }
+
+public:
+ MinimalStream()
+ : buffer_(stack_buffer_), size_(0), capacity_(STACK_BUFFER_SIZE), on_heap_(false) {}
+
+ ~MinimalStream() {
+ if (on_heap_) {
+ delete[] buffer_;
+ }
+ }
+
+ MinimalStream& operator<<(const std::string& s) {
+ append(s.c_str(), s.size());
+ return *this;
+ }
+
+ MinimalStream& operator<<(const char* s) {
+ append(s, std::strlen(s));
+ return *this;
+ }
+
+ MinimalStream& operator<<(char c) {
+ ensure_capacity(1);
+ buffer_[size_++] = c;
+ return *this;
+ }
+
+ MinimalStream& operator<<(int32_t i) {
+ print_integer(i);
+ return *this;
+ }
+
+ MinimalStream& operator<<(int64_t i) {
+ print_integer(i);
+ return *this;
+ }
+
+ MinimalStream& operator<<(uint32_t i) {
+ print_unsigned(i);
+ return *this;
+ }
+
+ MinimalStream& operator<<(uint64_t i) {
+ print_unsigned(i);
+ return *this;
+ }
+
+ MinimalStream& operator<<(double d) {
+ // For doubles, we still need sprintf for proper formatting
+ char temp[64];
+ int len = std::snprintf(temp, sizeof(temp), "%g", d);
+ if (len > 0) {
+ append(temp, len);
+ }
+ return *this;
+ }
+
+ MinimalStream& operator<<(bool b) {
+ if (b) {
+ append("true", 4);
+ } else {
+ append("false", 5);
+ }
+ return *this;
+ }
+
+ std::string str() const {
+ return std::string(buffer_, size_);
+ }
+
+ void clear() {
+ if (on_heap_) {
+ delete[] buffer_;
+ buffer_ = stack_buffer_;
+ capacity_ = STACK_BUFFER_SIZE;
+ on_heap_ = false;
+ }
+ size_ = 0;
+ }
+};
+
+int main() {
+ std::cout << "Testing template_streamop with ThriftTest types..." << std::endl;
+
+ // Test 1: Test with std::ostringstream
+ {
+ Xtruct x;
+ x.__set_string_thing("test string");
+ x.__set_byte_thing(42);
+ x.__set_i32_thing(12345);
+ x.__set_i64_thing(9876543210LL);
+
+ std::ostringstream oss;
+ oss << x;
+ std::string result = oss.str();
+
+ std::cout << " Generated output: " << result << std::endl;
+
+ assert(!result.empty());
+ assert(result.find("test string") != std::string::npos);
+ assert(result.find("42") != std::string::npos);
+ assert(result.find("12345") != std::string::npos);
+ std::cout << " ✓ std::ostringstream works: " << result << std::endl;
+ }
+
+ // Test 2: Test with custom MinimalStream
+ {
+ Xtruct x;
+ x.__set_string_thing("custom stream");
+ x.__set_byte_thing(7);
+ x.__set_i32_thing(999);
+ x.__set_i64_thing(1234567890LL);
+
+ MinimalStream ms;
+ ms << x;
+ std::string result = ms.str();
+
+ assert(!result.empty());
+ assert(result.find("custom stream") != std::string::npos);
+ assert(result.find("7") != std::string::npos);
+ assert(result.find("999") != std::string::npos);
+ std::cout << " ✓ MinimalStream works: " << result << std::endl;
+ }
+
+ // Test 3: Test nested structures
+ {
+ Xtruct x;
+ x.__set_string_thing("inner");
+ x.__set_i32_thing(100);
+
+ Xtruct2 x2;
+ x2.__set_byte_thing(5);
+ x2.__set_struct_thing(x);
+ x2.__set_i32_thing(200);
+
+ std::ostringstream oss;
+ oss << x2;
+ std::string result = oss.str();
+
+ assert(!result.empty());
+ assert(result.find("inner") != std::string::npos);
+ assert(result.find("100") != std::string::npos);
+ assert(result.find("200") != std::string::npos);
+ std::cout << " ✓ Nested structures work" << std::endl;
+ }
+
+ // Test 4: Test optional fields
+ {
+ Bonk bonk;
+ bonk.__set_message("test message");
+ bonk.__set_type(42);
+
+ std::ostringstream oss;
+ oss << bonk;
+ std::string result = oss.str();
+
+ assert(!result.empty());
+ assert(result.find("test message") != std::string::npos);
+ assert(result.find("42") != std::string::npos);
+ std::cout << " ✓ Optional fields work" << std::endl;
+ }
+
+ // Test 5: Test structs with map/set/list/vector
+ {
+ std::cout << "\n Testing collection types..." << std::endl;
+
+ // Create an Insanity struct with map and list
+ Insanity insanity;
+
+ // Add items to the map
+ std::map<Numberz::type, UserId> userMap;
+ userMap[Numberz::ONE] = 1;
+ userMap[Numberz::FIVE] = 5;
+ insanity.__set_userMap(userMap);
+
+ // Add items to the list
+ std::vector<Xtruct> xtructs;
+ Xtruct x1;
+ x1.__set_string_thing("first");
+ x1.__set_i32_thing(111);
+ xtructs.push_back(x1);
+
+ Xtruct x2;
+ x2.__set_string_thing("second");
+ x2.__set_i32_thing(222);
+ xtructs.push_back(x2);
+ insanity.__set_xtructs(xtructs);
+
+ // Test with std::ostringstream
+ std::ostringstream oss;
+ oss << insanity;
+ std::string result = oss.str();
+
+ std::cout << " std::ostringstream output: " << result << std::endl;
+ assert(!result.empty());
+ assert(result.find("Insanity") != std::string::npos);
+ assert(result.find("userMap") != std::string::npos);
+ assert(result.find("xtructs") != std::string::npos);
+
+ // Test with MinimalStream
+ MinimalStream ms;
+ ms << insanity;
+ std::string ms_result = ms.str();
+
+ std::cout << " MinimalStream output: " << ms_result << std::endl;
+ assert(!ms_result.empty());
+ assert(ms_result.find("Insanity") != std::string::npos);
+
+ std::cout << " ✓ Map/List collections work with both streams" << std::endl;
+ }
+
+ // Test 6: Test to_string compatibility with collection structs
+ {
+ std::cout << "\n Testing to_string with collection structs..." << std::endl;
+
+ Insanity insanity;
+ std::map<Numberz::type, UserId> userMap;
+ userMap[Numberz::TWO] = 2;
+ insanity.__set_userMap(userMap);
+
+ std::vector<Xtruct> xtructs;
+ Xtruct x;
+ x.__set_string_thing("test");
+ x.__set_i32_thing(42);
+ xtructs.push_back(x);
+ insanity.__set_xtructs(xtructs);
+
+ // to_string should work with the generated types
+ std::string str_result = apache::thrift::to_string(insanity);
+
+ std::cout << " to_string output: " << str_result << std::endl;
+ assert(!str_result.empty());
+ assert(str_result.find("Insanity") != std::string::npos);
+
+ std::cout << " ✓ to_string works with collection structs" << std::endl;
+ }
+
+ // Test 7: Test enum output - should print by name
+ {
+ std::cout << "\n Testing enum output..." << std::endl;
+
+ // Create a struct with an enum field
+ Insanity insanity;
+ std::map<Numberz::type, UserId> userMap;
+ userMap[Numberz::ONE] = 1;
+ userMap[Numberz::FIVE] = 5;
+ userMap[Numberz::TWO] = 2;
+ insanity.__set_userMap(userMap);
+
+ // Test with std::ostringstream
+ std::ostringstream oss;
+ oss << insanity;
+ std::string result = oss.str();
+
+ std::cout << " std::ostringstream output: " << result << std::endl;
+ assert(result.find("ONE") != std::string::npos || result.find("1") != std::string::npos);
+
+ // Test with MinimalStream
+ MinimalStream ms;
+ ms << insanity;
+ std::string ms_result = ms.str();
+
+ std::cout << " MinimalStream output: " << ms_result << std::endl;
+ assert(!ms_result.empty());
+
+ std::cout << " ✓ Enum fields output correctly" << std::endl;
+ }
+
+ // Test 8: Test floating point types
+ {
+ std::cout << "\n Testing floating point types..." << std::endl;
+
+ // Note: ThriftTest doesn't have a struct with float/double fields
+ // So we test directly with printTo
+ float f = 3.14159f;
+ double d = 2.71828;
+
+ // Test with std::ostringstream
+ std::ostringstream oss_f, oss_d;
+ apache::thrift::printTo(oss_f, f);
+ apache::thrift::printTo(oss_d, d);
+
+ std::string f_result = oss_f.str();
+ std::string d_result = oss_d.str();
+
+ std::cout << " float printTo: " << f_result << std::endl;
+ std::cout << " double printTo: " << d_result << std::endl;
+
+ assert(!f_result.empty());
+ assert(!d_result.empty());
+ assert(f_result.find("3.14") != std::string::npos || f_result.find("3,14") != std::string::npos);
+ assert(d_result.find("2.71") != std::string::npos || d_result.find("2,71") != std::string::npos);
+
+ // Test with MinimalStream
+ MinimalStream ms_f, ms_d;
+ apache::thrift::printTo(ms_f, f);
+ apache::thrift::printTo(ms_d, d);
+
+ std::cout << " MinimalStream float: " << ms_f.str() << std::endl;
+ std::cout << " MinimalStream double: " << ms_d.str() << std::endl;
+
+ assert(!ms_f.str().empty());
+ assert(!ms_d.str().empty());
+
+ std::cout << " ✓ Floating point types work correctly" << std::endl;
+ }
+
+ // Performance Test: Compare std::ostringstream vs MinimalStream
+ {
+ const int iterations = 10000;
+ Xtruct x;
+ x.__set_string_thing("performance test string");
+ x.__set_byte_thing(123);
+ x.__set_i32_thing(456789);
+ x.__set_i64_thing(9876543210LL);
+
+ // Test std::ostringstream performance
+ auto start_oss = std::chrono::high_resolution_clock::now();
+ std::string accumulated_result; // Prevent optimization by accumulating results
+ for (int i = 0; i < iterations; ++i) {
+ std::ostringstream oss;
+ oss << x;
+ accumulated_result += oss.str(); // Use result to prevent optimization
+ }
+ auto end_oss = std::chrono::high_resolution_clock::now();
+ auto duration_oss = std::chrono::duration_cast<std::chrono::microseconds>(end_oss - start_oss).count();
+
+ // Test MinimalStream performance
+ auto start_ms = std::chrono::high_resolution_clock::now();
+ accumulated_result.clear(); // Reuse for MinimalStream test
+ for (int i = 0; i < iterations; ++i) {
+ MinimalStream ms;
+ ms << x;
+ accumulated_result += ms.str(); // Use result to prevent optimization
+ }
+ auto end_ms = std::chrono::high_resolution_clock::now();
+ auto duration_ms = std::chrono::duration_cast<std::chrono::microseconds>(end_ms - start_ms).count();
+
+ std::cout << "\n Performance comparison (" << iterations << " iterations):" << std::endl;
+ std::cout << " std::ostringstream: " << duration_oss << " μs" << std::endl;
+ std::cout << " MinimalStream: " << duration_ms << " μs" << std::endl;
+
+ if (duration_ms < duration_oss) {
+ double improvement = ((double)(duration_oss - duration_ms) / duration_oss) * 100.0;
+ std::cout << " MinimalStream is " << improvement << "% faster" << std::endl;
+ } else {
+ double difference = ((double)(duration_ms - duration_oss) / duration_oss) * 100.0;
+ std::cout << " std::ostringstream is " << difference << "% faster" << std::endl;
+ }
+
+ std::cout << " ✓ Performance test completed" << std::endl;
+ }
+
+ std::cout << "\n✅ All template_streamop tests passed!" << std::endl;
+ return 0;
+}