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/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;
+}