diff --git a/include/wtf/concepts/concepts.hpp b/include/wtf/concepts/concepts.hpp index 8ad9481..f963a12 100644 --- a/include/wtf/concepts/concepts.hpp +++ b/include/wtf/concepts/concepts.hpp @@ -19,6 +19,7 @@ #include #include #include +#include /// Contains concepts used throughout the WTF library namespace wtf::concepts {} diff --git a/include/wtf/concepts/stream_insertion.hpp b/include/wtf/concepts/stream_insertion.hpp new file mode 100644 index 0000000..d143e0a --- /dev/null +++ b/include/wtf/concepts/stream_insertion.hpp @@ -0,0 +1,35 @@ +/* + * Copyright 2026 NWChemEx-Project + * + * Licensed 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 + +namespace wtf::concepts { + +/** @brief Determines if the type @p T can be inserted into an ostream. + * + * @tparam T Type to check for stream insertion support. + * + * The StreamInsertable concept is satisfied if an object of type @p T can be + * inserted into an std::ostream using the << operator. Falling usual C++ + * conventions, this usually means that an object of type @p T can be + * printed. + */ +template +concept StreamInsertable = requires(std::ostream& os, const T& obj) { + { os << obj } -> std::same_as; +}; + +} // namespace wtf::concepts diff --git a/include/wtf/fp/detail_/float_holder.hpp b/include/wtf/fp/detail_/float_holder.hpp index 0c5c948..4df4369 100644 --- a/include/wtf/fp/detail_/float_holder.hpp +++ b/include/wtf/fp/detail_/float_holder.hpp @@ -52,6 +52,9 @@ class FloatHolder { /// Type of a pointer to a const_float_view_type object using const_float_view_pointer = std::unique_ptr; + /// Type of a string representing the held floating-point value + using string_type = std::string; + /// Default virtual destructor virtual ~FloatHolder() = default; @@ -170,6 +173,18 @@ class FloatHolder { */ bool is_const() const { return is_const_(); } + /** @brief Returns a string-representation of the held value. + * + * This method is used to get a string-representation of the floating-point + * object held by *this. This method is implemented by calling + * to_string_(). + * + * @return A string representing the held floating-point value. + * + * @throw ??? Throws if converting the type-erased value to a string + */ + string_type to_string() const { return to_string_(); } + protected: /// Initializes *this with the given type info object explicit FloatHolder(type_info ti) : m_type_(std::move(ti)) {} @@ -193,6 +208,9 @@ class FloatHolder { /// Base checks that types are equal, derived need only check values virtual bool are_equal_(const FloatHolder& other) const = 0; + /// Used to implement to_string_ + virtual string_type to_string_() const = 0; + /// The RTTI object for the object held by the derived class type_info m_type_; }; diff --git a/include/wtf/fp/detail_/float_model.hpp b/include/wtf/fp/detail_/float_model.hpp index d6d0d1e..6196765 100644 --- a/include/wtf/fp/detail_/float_model.hpp +++ b/include/wtf/fp/detail_/float_model.hpp @@ -15,8 +15,10 @@ */ #pragma once +#include #include // for std::move, std::swap #include +#include #include #include #include @@ -203,6 +205,17 @@ class FloatModel : public FloatHolder { return false; } + /// Implements FloatHolder::to_string by using stringstream + string_type to_string_() const override { + if constexpr(concepts::StreamInsertable) { + std::stringstream ss; + ss << value_; + return ss.str(); + } else { + return ""; + } + } + /// The value being held value_type value_; }; diff --git a/include/wtf/fp/detail_/float_view_holder.hpp b/include/wtf/fp/detail_/float_view_holder.hpp index 1c080b5..242004f 100644 --- a/include/wtf/fp/detail_/float_view_holder.hpp +++ b/include/wtf/fp/detail_/float_view_holder.hpp @@ -54,6 +54,9 @@ class FloatViewHolder { /// Read-only reference to type_info object using const_type_info_reference = const type_info&; + /// Type of a string representation of *this + using string_type = std::string; + /// Default virtual destructor virtual ~FloatViewHolder() = default; @@ -177,6 +180,18 @@ class FloatViewHolder { } } + /** @brief Returns a string-representation of the held value. + * + * This method is used to get a string-representation of the floating-point + * object held by *this. This method is implemented by calling + * to_string_(). + * + * @return A string representing the held floating-point value. + * + * @throw ??? Throws if converting the type-erased value to a string + */ + string_type to_string() const { return to_string_(); } + protected: /// Initializes *this with the given type info object explicit FloatViewHolder(type_info ti) : m_type_(std::move(ti)) {} @@ -194,6 +209,9 @@ class FloatViewHolder { /// Base checks that types are equal, derived need only check values virtual bool are_equal_(const_holder_reference other) const = 0; + /// Derived class should override to provide string representation + virtual string_type to_string_() const = 0; + /// The RTTI object for the object held by the derived class type_info m_type_; }; diff --git a/include/wtf/fp/detail_/float_view_model.hpp b/include/wtf/fp/detail_/float_view_model.hpp index 8674579..6b406ca 100644 --- a/include/wtf/fp/detail_/float_view_model.hpp +++ b/include/wtf/fp/detail_/float_view_model.hpp @@ -17,6 +17,7 @@ #pragma once #include // for std::move, std::swap #include +#include #include #include #include @@ -58,6 +59,7 @@ class FloatViewModel ///@{ using const_holder_type = typename holder_type::const_holder_type; using typename holder_type::const_holder_reference; + using typename holder_type::string_type; ///@} /// Type defining the traits for FloatType @@ -214,7 +216,7 @@ class FloatViewModel } } - /// Implements FloatHolder::are_equal by downcasting and calling operator== + /// Implements are_equal by downcasting and calling operator== bool are_equal_(const_holder_reference other) const override { if(auto* p = dynamic_cast(&other)) { return *this == *p; @@ -222,6 +224,17 @@ class FloatViewModel return false; } + /// Implements FloatViewHolder::to_string by using stringstream + string_type to_string_() const override { + if constexpr(concepts::StreamInsertable) { + std::stringstream ss; + ss << *m_pvalue_; + return ss.str(); + } else { + return ""; + } + } + /// The value being aliased pointer m_pvalue_; }; diff --git a/include/wtf/fp/float.hpp b/include/wtf/fp/float.hpp index 12d5fd2..8ecec1c 100644 --- a/include/wtf/fp/float.hpp +++ b/include/wtf/fp/float.hpp @@ -44,6 +44,9 @@ class Float { /// Type of a read-only view acting like *this using const_view_type = FloatView; + /// Type of converting *this to a string + using string_type = holder_type::string_type; + // ------------------------------------------------------------------------- // Ctors and assignment operators // ------------------------------------------------------------------------- @@ -201,7 +204,23 @@ class Float { */ bool operator!=(const Float& other) const { return !(*this == other); } + /** @brief Provides a string representation of the held value. + * + * If *this is not holding a value, an empty string is returned. + * + * @return A string representation of the held value. + * + * @throw ??? Throws if converting the held value to a string throws. + * Strong throw guarantee. + */ + string_type to_string() const { + return is_holding_() ? m_holder_->to_string() : string_type{}; + } + private: + /// Determines if *this is holding a value or not + bool is_holding_() const noexcept { return m_holder_ != nullptr; } + template friend auto visit_float(Visitor&& visitor, Args&&... args); diff --git a/include/wtf/fp/float_view.hpp b/include/wtf/fp/float_view.hpp index d725f16..588b134 100644 --- a/include/wtf/fp/float_view.hpp +++ b/include/wtf/fp/float_view.hpp @@ -71,6 +71,7 @@ class FloatView { /// Add types from holder_type to API ///@{ using holder_pointer = typename holder_type::holder_pointer; + using string_type = typename holder_type::string_type; ///@} /** @brief Creates a FloatView that aliases @p value. @@ -353,6 +354,19 @@ class FloatView { (concepts::ConstQualified || concepts::Unmodified)) T value() const; + /** @brief Provides a string representation of the held value. + * + * If *this is not holding a value, an empty string is returned. + * + * @return A string representation of the held value. + * + * @throw ??? Throws if converting the held value to a string throws. + * Strong throw guarantee. + */ + string_type to_string() const { + return is_holding_() ? m_pfloat_->to_string() : string_type{}; + } + private: template friend class FloatView; @@ -360,6 +374,9 @@ class FloatView { template friend auto make_float_view(T& value); + /// Determines if *this is holding a value or not + bool is_holding_() const noexcept { return m_pfloat_ != nullptr; } + /// The holder object holder_pointer m_pfloat_; }; diff --git a/tests/unit_tests/wtf/concepts/stream_insertion.cpp b/tests/unit_tests/wtf/concepts/stream_insertion.cpp new file mode 100644 index 0000000..80d7bf4 --- /dev/null +++ b/tests/unit_tests/wtf/concepts/stream_insertion.cpp @@ -0,0 +1,39 @@ +/* + * Copyright 2026 NWChemEx-Project + * + * Licensed 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 "../../../test_wtf.hpp" +#include + +namespace { + +struct Insertable {}; + +[[maybe_unused]] std::ostream& operator<<(std::ostream& os, const Insertable&) { + return os; +} + +struct NotInsertable {}; + +} // namespace + +TEST_CASE("StreamInsertable concept", "[wtf][concepts]") { + using namespace wtf::concepts; + + STATIC_REQUIRE(StreamInsertable); + STATIC_REQUIRE_FALSE(StreamInsertable); + STATIC_REQUIRE(StreamInsertable); + STATIC_REQUIRE(StreamInsertable); +} diff --git a/tests/unit_tests/wtf/fp/detail_/float_holder.cpp b/tests/unit_tests/wtf/fp/detail_/float_holder.cpp index 6eff9ad..e5e928a 100644 --- a/tests/unit_tests/wtf/fp/detail_/float_holder.cpp +++ b/tests/unit_tests/wtf/fp/detail_/float_holder.cpp @@ -65,4 +65,14 @@ TEMPLATE_LIST_TEST_CASE("FloatHolder", "[wtf]", all_fp_types) { holder_t& h4 = m4; REQUIRE_FALSE(h.are_equal(h4)); } + + SECTION("to_string") { + if constexpr(wtf::concepts::StreamInsertable) { + std::stringstream ss; + ss << val; + REQUIRE(h.to_string() == ss.str()); + } else { + REQUIRE(h.to_string() == ""); + } + } } diff --git a/tests/unit_tests/wtf/fp/detail_/float_model.cpp b/tests/unit_tests/wtf/fp/detail_/float_model.cpp index 7595cc9..dde7eb9 100644 --- a/tests/unit_tests/wtf/fp/detail_/float_model.cpp +++ b/tests/unit_tests/wtf/fp/detail_/float_model.cpp @@ -118,6 +118,16 @@ TEMPLATE_LIST_TEST_CASE("FloatModel", "[float_model]", test_wtf::all_fp_types) { FloatModel m4(other_type(3.14)); REQUIRE_FALSE(m.are_equal(m4)); } + + SECTION("to_string_") { + if constexpr(wtf::concepts::StreamInsertable) { + std::stringstream ss; + ss << val; + REQUIRE(m.to_string() == ss.str()); + } else { + REQUIRE(m.to_string() == ""); + } + } } TEMPLATE_LIST_TEST_CASE("visit_float_model", "[float_model]", diff --git a/tests/unit_tests/wtf/fp/detail_/float_view_holder.cpp b/tests/unit_tests/wtf/fp/detail_/float_view_holder.cpp index 660e0a2..167981a 100644 --- a/tests/unit_tests/wtf/fp/detail_/float_view_holder.cpp +++ b/tests/unit_tests/wtf/fp/detail_/float_view_holder.cpp @@ -113,4 +113,16 @@ TEMPLATE_LIST_TEST_CASE("FloatViewHolder", "[wtf]", all_fp_types) { // Different const-ness REQUIRE_FALSE(h.are_equal(ch)); } + + SECTION("to_string") { + if constexpr(wtf::concepts::StreamInsertable) { + std::stringstream ss; + ss << val; + REQUIRE(h.to_string() == ss.str()); + REQUIRE(ch.to_string() == ss.str()); + } else { + REQUIRE(h.to_string() == ""); + REQUIRE(ch.to_string() == ""); + } + } } diff --git a/tests/unit_tests/wtf/fp/detail_/float_view_model.cpp b/tests/unit_tests/wtf/fp/detail_/float_view_model.cpp index 524ac8c..eb8feff 100644 --- a/tests/unit_tests/wtf/fp/detail_/float_view_model.cpp +++ b/tests/unit_tests/wtf/fp/detail_/float_view_model.cpp @@ -180,4 +180,16 @@ TEMPLATE_LIST_TEST_CASE("FloatViewModel", "[float_model]", // Different const-ness REQUIRE_FALSE(m.are_equal(cm)); } + + SECTION("to_string_") { + if constexpr(wtf::concepts::StreamInsertable) { + std::stringstream ss; + ss << val; + REQUIRE(m.to_string() == ss.str()); + REQUIRE(cm.to_string() == ss.str()); + } else { + REQUIRE(m.to_string() == ""); + REQUIRE(cm.to_string() == ""); + } + } } diff --git a/tests/unit_tests/wtf/fp/float.cpp b/tests/unit_tests/wtf/fp/float.cpp index d66d13a..c7a4521 100644 --- a/tests/unit_tests/wtf/fp/float.cpp +++ b/tests/unit_tests/wtf/fp/float.cpp @@ -125,6 +125,16 @@ TEMPLATE_LIST_TEST_CASE("Float", "[wtf]", test_wtf::all_fp_types) { Float f2(other_val); REQUIRE(f != f2); } + + SECTION("to_string") { + if constexpr(wtf::concepts::StreamInsertable) { + std::stringstream ss; + ss << val; + REQUIRE(f.to_string() == ss.str()); + } else { + REQUIRE(f.to_string() == ""); + } + } } TEST_CASE("make_float", "[wtf]") { diff --git a/tests/unit_tests/wtf/fp/float_view.cpp b/tests/unit_tests/wtf/fp/float_view.cpp index beecd48..4b75ce8 100644 --- a/tests/unit_tests/wtf/fp/float_view.cpp +++ b/tests/unit_tests/wtf/fp/float_view.cpp @@ -274,6 +274,18 @@ TEMPLATE_LIST_TEST_CASE("FloatView", "[wtf]", test_wtf::all_fp_types) { REQUIRE_THROWS_AS(f_const.template value(), std::runtime_error); } + + SECTION("to_string") { + if constexpr(wtf::concepts::StreamInsertable) { + std::stringstream ss; + ss << val; + REQUIRE(f.to_string() == ss.str()); + REQUIRE(cf.to_string() == ss.str()); + } else { + REQUIRE(f.to_string() == ""); + REQUIRE(cf.to_string() == ""); + } + } } TEST_CASE("float_cast(FloatView)", "[wtf]") {