diff --git a/compiler/cpp/src/thrift/generate/t_cpp_generator.cc b/compiler/cpp/src/thrift/generate/t_cpp_generator.cc index 99919e244f..acc40c0ecd 100644 --- a/compiler/cpp/src/thrift/generate/t_cpp_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_cpp_generator.cc @@ -66,6 +66,7 @@ class t_cpp_generator : public t_oop_generator { gen_templates_only_ = false; gen_moveable_ = false; gen_forward_setter_ = false; + gen_template_streamop_ = false; gen_no_ostream_operators_ = false; gen_no_skeleton_ = false; gen_no_constructors_ = false; @@ -96,6 +97,8 @@ class t_cpp_generator : public t_oop_generator { } } else if ( iter->first.compare("no_ostream_operators") == 0) { gen_no_ostream_operators_ = true; + } else if ( iter->first.compare("template_streamop") == 0) { + gen_template_streamop_ = true; } else if ( iter->first.compare("no_skeleton") == 0) { gen_no_skeleton_ = true; } else if ( iter->first.compare("no_constructors") == 0) { @@ -130,6 +133,8 @@ class t_cpp_generator : public t_oop_generator { void generate_enum_ostream_operator(std::ostream& out, t_enum* tenum); void generate_enum_to_string_helper_function_decl(std::ostream& out, t_enum* tenum); void generate_enum_to_string_helper_function(std::ostream& out, t_enum* tenum); + void generate_enum_printto_helper_function_decl(std::ostream& out, t_enum* tenum); + void generate_enum_printto_helper_function(std::ostream& out, t_enum* tenum); void generate_forward_declaration(t_struct* tstruct) override; void generate_struct(t_struct* tstruct) override { generate_cpp_struct(tstruct, false); } void generate_xception(t_struct* txception) override { generate_cpp_struct(txception, true); } @@ -371,6 +376,11 @@ class t_cpp_generator : public t_oop_generator { */ bool gen_forward_setter_; + /** + * True if we should generate operator<< and printTo with generic stream type template. + */ + bool gen_template_streamop_; + /** * True if we should generate ostream definitions */ @@ -462,7 +472,7 @@ void t_cpp_generator::init_generator() { string f_types_impl_name = get_out_dir() + program_name_ + "_types.cpp"; f_types_impl_.open(f_types_impl_name.c_str()); - if (gen_templates_ || gen_forward_setter_) { + if (gen_templates_ || gen_forward_setter_ || gen_template_streamop_) { // If we don't open the stream, it appears to just discard data, // which is fine. string f_types_tcc_name = get_out_dir() + program_name_ + "_types.tcc"; @@ -529,6 +539,12 @@ void t_cpp_generator::init_generator() { f_types_impl_ << "#include " << '\n' << '\n'; f_types_impl_ << "#include " << '\n' << '\n'; + // For template_streamop, we need TPrintTo.h in the .tcc file for direct streaming + // TPrintTo avoids the overhead of to_string which uses ostringstream internally + if (gen_template_streamop_) { + f_types_tcc_ << "#include " << '\n' << '\n'; + } + // Open namespace ns_open_ = namespace_open(program_->get_namespace("cpp")); ns_close_ = namespace_close(program_->get_namespace("cpp")); @@ -552,7 +568,7 @@ void t_cpp_generator::close_generator() { // Include the types.tcc file from the types header file, // so clients don't have to explicitly include the tcc file. // TODO(simpkins): Make this a separate option. - if (gen_templates_ || gen_forward_setter_) { + if (gen_templates_ || gen_forward_setter_ || gen_template_streamop_) { f_types_ << "#include \"" << get_include_prefix(*get_program()) << program_name_ << "_types.tcc\"" << '\n' << '\n'; } @@ -680,6 +696,12 @@ void t_cpp_generator::generate_enum(t_enum* tenum) { generate_enum_to_string_helper_function_decl(f_types_, tenum); generate_enum_to_string_helper_function(f_types_impl_, tenum); + // Generate template printTo specialization for enums when template_streamop is enabled + if (gen_template_streamop_) { + generate_enum_printto_helper_function_decl(f_types_, tenum); + generate_enum_printto_helper_function(f_types_tcc_, tenum); + } + has_members_ = true; } @@ -777,6 +799,52 @@ void t_cpp_generator::generate_enum_to_string_helper_function(std::ostream& out, } } +void t_cpp_generator::generate_enum_printto_helper_function_decl(std::ostream& out, t_enum* tenum) { + out << "template " << '\n'; + out << "void printTo(OStream_& out, const "; + if (gen_pure_enums_) { + out << tenum->get_name(); + } else { + out << tenum->get_name() << "::type&"; + } + out << " val);" << '\n'; + out << '\n'; +} + +void t_cpp_generator::generate_enum_printto_helper_function(std::ostream& out, t_enum* tenum) { + if (!has_custom_ostream(tenum)) { + out << "template " << '\n'; + out << "void printTo(OStream_& out, const "; + if (gen_pure_enums_) { + out << tenum->get_name(); + } else { + out << tenum->get_name() << "::type&"; + } + out << " val) "; + scope_up(out); + + out << indent() << "std::map::const_iterator it = _" + << tenum->get_name() << "_VALUES_TO_NAMES.find("; + if (gen_enum_class_) { + out << "static_cast(val));" << '\n'; + } else { + out << "val);" << '\n'; + } + out << indent() << "if (it != _" << tenum->get_name() << "_VALUES_TO_NAMES.end()) {" << '\n'; + indent_up(); + out << indent() << "out << it->second;" << '\n'; + indent_down(); + out << indent() << "} else {" << '\n'; + indent_up(); + out << indent() << "out << static_cast(val);" << '\n'; + indent_down(); + out << indent() << "}" << '\n'; + + scope_down(out); + out << '\n'; + } +} + /** * Generates a class that holds all the constants. */ @@ -1003,7 +1071,9 @@ void t_cpp_generator::generate_cpp_struct(t_struct* tstruct, bool is_exception) } if (!has_custom_ostream(tstruct)) { - generate_struct_print_method(f_types_impl_, tstruct); + // When template_streamop is enabled, printTo implementation goes to .tcc file + std::ostream& print_method_out = (gen_template_streamop_ ? f_types_tcc_ : f_types_impl_); + generate_struct_print_method(print_method_out, tstruct); } if (is_exception) { @@ -1481,7 +1551,8 @@ void t_cpp_generator::generate_struct_declaration(ostream& out, if (is_user_struct && !has_custom_ostream(tstruct)) { out << indent(); - if (!gen_templates_) out << "virtual "; + // Template methods cannot be virtual, so skip virtual keyword when using template_streamop + if (!gen_templates_ && !gen_template_streamop_) out << "virtual "; generate_struct_print_method_decl(out, nullptr); out << ";" << '\n'; } @@ -1532,7 +1603,9 @@ void t_cpp_generator::generate_struct_declaration(ostream& out, // When private_optional is enabled, optional members may be private. // The generated namespace-scope operator<< needs friend access. if (is_user_struct && gen_private_optional_) { - indent(out) << "friend "; + if (!gen_template_streamop_) { + indent(out) << "friend "; + } generate_struct_ostream_operator_decl(out, tstruct); } @@ -1610,7 +1683,9 @@ void t_cpp_generator::generate_struct_definition(ostream& out, } } if (is_user_struct) { - generate_struct_ostream_operator(out, tstruct); + // When template_streamop is enabled, operator<< implementation goes to .tcc file + std::ostream& ostream_op_out = (gen_template_streamop_ ? f_types_tcc_ : out); + generate_struct_ostream_operator(ostream_op_out, tstruct); } out << '\n'; } @@ -1979,18 +2054,35 @@ void t_cpp_generator::generate_struct_swap_decl(std::ostream& out, t_struct* tst } void t_cpp_generator::generate_struct_ostream_operator_decl(std::ostream& out, t_struct* tstruct) { - out << "std::ostream& operator<<(std::ostream& out, const " - << tstruct->get_name() - << "& obj);" << '\n'; + if (gen_template_streamop_) { + out << "template " << '\n'; + if (gen_private_optional_) { + out << indent() << "friend "; + } + out << "OStream_& operator<<(OStream_& out, const " + << tstruct->get_name() + << "& obj);" << '\n'; + } else { + out << "std::ostream& operator<<(std::ostream& out, const " + << tstruct->get_name() + << "& obj);" << '\n'; + } out << '\n'; } void t_cpp_generator::generate_struct_ostream_operator(std::ostream& out, t_struct* tstruct) { if (!has_custom_ostream(tstruct)) { // thrift defines this behavior - out << "std::ostream& operator<<(std::ostream& out, const " - << tstruct->get_name() - << "& obj)" << '\n'; + if (gen_template_streamop_) { + out << "template " << '\n'; + out << "OStream_& operator<<(OStream_& out, const " + << tstruct->get_name() + << "& obj)" << '\n'; + } else { + out << "std::ostream& operator<<(std::ostream& out, const " + << tstruct->get_name() + << "& obj)" << '\n'; + } scope_up(out); out << indent() << "obj.printTo(out);" << '\n' << indent() << "return out;" << '\n'; @@ -2000,11 +2092,24 @@ void t_cpp_generator::generate_struct_ostream_operator(std::ostream& out, t_stru } void t_cpp_generator::generate_struct_print_method_decl(std::ostream& out, t_struct* tstruct) { - out << "void "; - if (tstruct) { - out << tstruct->get_name() << "::"; + if (gen_template_streamop_) { + // For template version, the method itself is templated + if (!tstruct) { + // Declaration inside class - no "template" keyword here, will be added by caller if needed + out << "template " << '\n' << indent() << "void "; + } else { + // External implementation - needs template keyword + out << "template " << '\n' << indent() << "void "; + out << tstruct->get_name() << "::"; + } + out << "printTo(OStream_& out) const"; + } else { + out << "void "; + if (tstruct) { + out << tstruct->get_name() << "::"; + } + out << "printTo(std::ostream& out) const"; } - out << "printTo(std::ostream& out) const"; } void t_cpp_generator::generate_exception_what_method_decl(std::ostream& out, @@ -2020,35 +2125,49 @@ void t_cpp_generator::generate_exception_what_method_decl(std::ostream& out, } namespace struct_ostream_operator_generator { -void generate_required_field_value(std::ostream& out, const t_field* field) { +void generate_required_field_value(std::ostream& out, const t_field* field, bool use_printto) { + if (use_printto) { + // For template_streamop, use printTo for direct streaming without temporary strings + // Use comma operator: out << "x=", printTo(out, x) + out << ", printTo(out, " << field->get_name() << ")"; + return; + } + // For std::ostream, use to_string (backward compatible) out << " << to_string(" << field->get_name() << ")"; } -void generate_optional_field_value(std::ostream& out, const t_field* field) { - out << "; (__isset." << field->get_name() << " ? (out"; - generate_required_field_value(out, field); - out << ") : (out << \"\"))"; +void generate_optional_field_value(std::ostream& out, const t_field* field, bool use_printto) { + out << "; (__isset." << field->get_name() << " ? "; + if (use_printto) { + // For printTo, call directly without wrapping in (out ...) + out << "printTo(out, " << field->get_name() << ")"; + } else { + // For to_string, need to wrap with (out << ...) + out << "(out << to_string(" << field->get_name() << "))"; + } + out << " : (out << \"\"))"; } -void generate_field_value(std::ostream& out, const t_field* field) { +void generate_field_value(std::ostream& out, const t_field* field, bool use_printto) { if (field->get_req() == t_field::T_OPTIONAL) - generate_optional_field_value(out, field); + generate_optional_field_value(out, field, use_printto); else - generate_required_field_value(out, field); + generate_required_field_value(out, field, use_printto); } void generate_field_name(std::ostream& out, const t_field* field) { out << "\"" << field->get_name() << "=\""; } -void generate_field(std::ostream& out, const t_field* field) { +void generate_field(std::ostream& out, const t_field* field, bool use_printto) { generate_field_name(out, field); - generate_field_value(out, field); + generate_field_value(out, field, use_printto); } void generate_fields(std::ostream& out, const vector& fields, - const std::string& indent) { + const std::string& indent, + bool use_printto) { const vector::const_iterator beg = fields.begin(); const vector::const_iterator end = fields.end(); @@ -2059,7 +2178,7 @@ void generate_fields(std::ostream& out, out << "\", \" << "; } - generate_field(out, *it); + generate_field(out, *it, use_printto); out << ";" << '\n'; } } @@ -2075,9 +2194,16 @@ void t_cpp_generator::generate_struct_print_method(std::ostream& out, t_struct* indent_up(); + bool use_printto = gen_template_streamop_; + if (use_printto) { + // For template_streamop, use printTo for direct streaming (better performance) + out << indent() << "using ::apache::thrift::printTo;" << '\n'; + } + // Always include to_string as well for compatibility out << indent() << "using ::apache::thrift::to_string;" << '\n'; + out << indent() << "out << \"" << tstruct->get_name() << "(\";" << '\n'; - struct_ostream_operator_generator::generate_fields(out, tstruct->get_members(), indent()); + struct_ostream_operator_generator::generate_fields(out, tstruct->get_members(), indent(), use_printto); out << indent() << "out << \")\";" << '\n'; indent_down(); diff --git a/compiler/cpp/tests/cpp/t_cpp_generator_template_streamop_tests.cc b/compiler/cpp/tests/cpp/t_cpp_generator_template_streamop_tests.cc new file mode 100644 index 0000000000..bb17b23276 --- /dev/null +++ b/compiler/cpp/tests/cpp/t_cpp_generator_template_streamop_tests.cc @@ -0,0 +1,166 @@ +// 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 "t_cpp_generator_test_utils.h" + +using std::string; +using std::map; +using cpp_generator_test_utils::read_file; +using cpp_generator_test_utils::source_dir; +using cpp_generator_test_utils::join_path; +using cpp_generator_test_utils::normalize_for_compare; +using cpp_generator_test_utils::parse_thrift_for_test; + +// Helper function declared in t_cpp_generator_private_optional_tests.cc +// Extracts the class definition from generated content for the given class name +extern string extract_class_definition(const string& content, const string& class_name); + +TEST_CASE("t_cpp_generator without template_streamop generates standard operator<< and printTo", "[functional]") +{ + string path = join_path(source_dir(), "test_template_streamop.thrift"); + string name = "test_template_streamop"; + map parsed_options = {}; + string option_string = ""; + + std::unique_ptr program(new t_program(path, name)); + parse_thrift_for_test(program.get()); + + std::unique_ptr gen( + t_generator_registry::get_generator(program.get(), "cpp", parsed_options, option_string)); + REQUIRE(gen != nullptr); + + // Generate code + REQUIRE_NOTHROW(gen->generate_program()); + + // Read generated output + string generated_file = "gen-cpp/test_template_streamop_types.h"; + string generated_content = read_file(generated_file); + REQUIRE(!generated_content.empty()); + + // Verify operator<< declaration uses std::ostream (not template) + REQUIRE(generated_content.find("std::ostream& operator<<(std::ostream& out, const SimpleStruct& obj);") != string::npos); + + // Extract class definition to check printTo + string class_def = extract_class_definition(generated_content, "SimpleStruct"); + REQUIRE(!class_def.empty()); + + // Verify printTo method uses std::ostream (not template) + REQUIRE(class_def.find("void printTo(std::ostream& out) const;") != string::npos); + + // Verify no template syntax for printTo or operator<< + REQUIRE(class_def.find("template ") == string::npos); + + // Read implementation file + string impl_file = "gen-cpp/test_template_streamop_types.cpp"; + string impl_content = read_file(impl_file); + REQUIRE(!impl_content.empty()); + + // Verify implementation also uses std::ostream + REQUIRE(impl_content.find("void SimpleStruct::printTo(std::ostream& out) const") != string::npos); + REQUIRE(impl_content.find("std::ostream& operator<<(std::ostream& out, const SimpleStruct& obj)") != string::npos); +} + +TEST_CASE("t_cpp_generator with template_streamop generates templated operator<< and printTo", "[functional]") +{ + string path = join_path(source_dir(), "test_template_streamop.thrift"); + string name = "test_template_streamop"; + map parsed_options = {{"template_streamop", ""}}; + string option_string = ""; + + std::unique_ptr program(new t_program(path, name)); + parse_thrift_for_test(program.get()); + + std::unique_ptr gen( + t_generator_registry::get_generator(program.get(), "cpp", parsed_options, option_string)); + REQUIRE(gen != nullptr); + + // Generate code + REQUIRE_NOTHROW(gen->generate_program()); + + // Read generated header + string generated_file = "gen-cpp/test_template_streamop_types.h"; + string generated_content = read_file(generated_file); + REQUIRE(!generated_content.empty()); + + // Verify operator<< declaration uses template syntax + REQUIRE(generated_content.find("template ") != string::npos); + REQUIRE(generated_content.find("OStream_& operator<<(OStream_& out, const SimpleStruct& obj);") != string::npos); + + // Extract class definition + string class_def = extract_class_definition(generated_content, "SimpleStruct"); + REQUIRE(!class_def.empty()); + + // Verify printTo method declaration uses template syntax + REQUIRE(class_def.find("template ") != string::npos); + REQUIRE(class_def.find("void printTo(OStream_& out) const;") != string::npos); + + // Verify no hardcoded std::ostream in printTo declaration + REQUIRE(class_def.find("void printTo(std::ostream& out) const;") == string::npos); + + // Read implementation file (.tcc for templates) + string impl_file = "gen-cpp/test_template_streamop_types.tcc"; + string impl_content = read_file(impl_file); + REQUIRE(!impl_content.empty()); + + // Verify implementation uses template syntax + REQUIRE(impl_content.find("template ") != string::npos); + REQUIRE(impl_content.find("void SimpleStruct::printTo(OStream_& out) const") != string::npos); + REQUIRE(impl_content.find("OStream_& operator<<(OStream_& out, const SimpleStruct& obj)") != string::npos); + + // Verify both SimpleStruct and NestedStruct have template versions + REQUIRE(impl_content.find("void NestedStruct::printTo(OStream_& out) const") != string::npos); +} + +TEST_CASE("t_cpp_generator with template_streamop and private_optional generates correct friend declarations", "[functional]") +{ + string path = join_path(source_dir(), "test_template_streamop.thrift"); + string name = "test_template_streamop"; + map parsed_options = {{"template_streamop", ""}, {"private_optional", ""}}; + string option_string = ""; + + std::unique_ptr program(new t_program(path, name)); + parse_thrift_for_test(program.get()); + + std::unique_ptr gen( + t_generator_registry::get_generator(program.get(), "cpp", parsed_options, option_string)); + REQUIRE(gen != nullptr); + + // Generate code + REQUIRE_NOTHROW(gen->generate_program()); + + // Read generated header + string generated_file = "gen-cpp/test_template_streamop_types.h"; + string generated_content = read_file(generated_file); + REQUIRE(!generated_content.empty()); + + // Extract class definition + string class_def = extract_class_definition(generated_content, "SimpleStruct"); + REQUIRE(!class_def.empty()); + + // Verify friend declaration comes after template keyword (correct C++ syntax) + // Should be: template + // friend OStream_& operator<<(...) + // NOT: friend template ... + REQUIRE(class_def.find("template ") != string::npos); + REQUIRE(class_def.find("friend OStream_& operator<<(OStream_& out, const SimpleStruct& obj);") != string::npos); + + // Verify incorrect syntax is not present + REQUIRE(class_def.find("friend template ") == string::npos); +} + +// Note: Enum printTo specialization functionality is tested in the runtime test (TemplateStreamOpTest) +// since the compiler test infrastructure doesn't support this testing pattern. diff --git a/compiler/cpp/tests/cpp/test_template_streamop.thrift b/compiler/cpp/tests/cpp/test_template_streamop.thrift new file mode 100644 index 0000000000..1d6c0c8455 --- /dev/null +++ b/compiler/cpp/tests/cpp/test_template_streamop.thrift @@ -0,0 +1,40 @@ +// 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. + +namespace cpp test.template_streamop + +enum Status { + ACTIVE = 1, + INACTIVE = 2, + PENDING = 3, +} + +struct SimpleStruct { + 1: i32 id; + 2: string name; + 3: optional string description; +} + +struct NestedStruct { + 1: i32 value; + 2: SimpleStruct inner; +} + +struct StructWithEnum { + 1: Status status; + 2: string name; +} diff --git a/lib/cpp/Makefile.am b/lib/cpp/Makefile.am index 3d7beab68a..dcbcf2c5db 100644 --- a/lib/cpp/Makefile.am +++ b/lib/cpp/Makefile.am @@ -143,6 +143,7 @@ include_thrift_HEADERS = \ src/thrift/TProcessor.h \ src/thrift/TApplicationException.h \ src/thrift/TLogging.h \ + src/thrift/TPrintTo.h \ src/thrift/TToString.h \ src/thrift/TBase.h \ src/thrift/TConfiguration.h \ diff --git a/lib/cpp/src/thrift/TPrintTo.h b/lib/cpp/src/thrift/TPrintTo.h new file mode 100644 index 0000000000..d3d5cb4348 --- /dev/null +++ b/lib/cpp/src/thrift/TPrintTo.h @@ -0,0 +1,97 @@ +/* + * 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. + */ + +#ifndef _THRIFT_TPRINTTO_H_ +#define _THRIFT_TPRINTTO_H_ 1 + +#include +#include +#include + +namespace apache { +namespace thrift { + +// Generic printTo template - streams value directly to output +template +void printTo(OStream& out, const T& t) { + out << t; +} + +// Special handling of i8 datatypes (THRIFT-5272) - cast to int to avoid char output +template +void printTo(OStream& out, const int8_t& t) { + out << static_cast(t); +} + +// Forward declarations for collection types +template +void printTo(OStream& out, const std::map& m); + +template +void printTo(OStream& out, const std::set& s); + +template +void printTo(OStream& out, const std::vector& t); + +// Pair support +template +void printTo(OStream& out, const std::pair& v) { + printTo(out, v.first); + out << ": "; + printTo(out, v.second); +} + +// Iterator range support +template +void printTo(OStream& out, Iterator beg, Iterator end) { + for (Iterator it = beg; it != end; ++it) { + if (it != beg) + out << ", "; + printTo(out, *it); + } +} + +// Vector support +template +void printTo(OStream& out, const std::vector& t) { + out << "["; + printTo(out, t.begin(), t.end()); + out << "]"; +} + +// Map support +template +void printTo(OStream& out, const std::map& m) { + out << "{"; + printTo(out, m.begin(), m.end()); + out << "}"; +} + +// Set support +template +void printTo(OStream& out, const std::set& s) { + out << "{"; + printTo(out, s.begin(), s.end()); + out << "}"; +} + +} // namespace thrift +} // namespace apache + +#endif // _THRIFT_TPRINTTO_H_ diff --git a/test/cpp/CMakeLists.txt b/test/cpp/CMakeLists.txt index 2403c87d9c..6fdc95e5e2 100644 --- a/test/cpp/CMakeLists.txt +++ b/test/cpp/CMakeLists.txt @@ -164,6 +164,29 @@ target_link_libraries(EnumClassTest enumclasstestgencpp ${Boost_LIBRARIES}) 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 @@ add_custom_command(OUTPUT gen-cpp-enumclass/gen-cpp/ThriftTest_types.cpp gen-cpp 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 f2d07ecc92..720a4b345b 100644 --- a/test/cpp/Makefile.am +++ b/test/cpp/Makefile.am @@ -28,7 +28,10 @@ BUILT_SOURCES = gen-cpp/ThriftTest.cpp \ 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 @@ libtestgencpp_la_LIBADD = $(top_builddir)/lib/cpp/libthrift.la 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 @@ nodist_libenumclasstestgencpp_la_SOURCES = \ 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 @@ check_PROGRAMS = \ StressTestNonBlocking \ ForwardSetterTest \ PrivateOptionalTest \ - EnumClassTest + EnumClassTest \ + TemplateStreamOpTest # we currently do not run the testsuite, stop c++ server issue # TESTS = \ @@ -162,6 +177,14 @@ EnumClassTest_LDADD = \ 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 @@ gen-cpp-enumclass/ThriftTest_types.cpp gen-cpp-enumclass/ThriftTest_types.h gen- $(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_CXXFLAGS = -Wall -Wextra -pedantic -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACR 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 @@ EXTRA_DIST = \ 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 3b21063034..673d6fb53e 100644 --- a/test/cpp/src/EnumClassTest.cpp +++ b/test/cpp/src/EnumClassTest.cpp @@ -23,8 +23,10 @@ */ #include +#include #include #include +#include // Include generated thrift types with enum_class option #include "ThriftTest_types.h" @@ -86,7 +88,88 @@ int main() { 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(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(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(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 0000000000..d97288d445 --- /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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Include generated thrift types with template_streamop option +#include "ThriftTest_types.h" +#include + +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 + 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 + 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 userMap; + userMap[Numberz::ONE] = 1; + userMap[Numberz::FIVE] = 5; + insanity.__set_userMap(userMap); + + // Add items to the list + std::vector 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 userMap; + userMap[Numberz::TWO] = 2; + insanity.__set_userMap(userMap); + + std::vector 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 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(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(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; +}