From 4e4446eb97bb67454ab36db82aad0914724370a9 Mon Sep 17 00:00:00 2001 From: Stephen Berry Date: Thu, 2 Jan 2025 12:37:02 -0600 Subject: [PATCH] Improved async_string with more methods and std::format support (#1536) * Improved async_string with more methods and std::format support * Update async_string.hpp * resize and reserve --- include/glaze/concepts/container_concepts.hpp | 1 + include/glaze/thread/async_string.hpp | 133 ++++++++++++--- tests/exceptions_test/exceptions_test.cpp | 160 +++++++++++++++--- 3 files changed, 247 insertions(+), 47 deletions(-) diff --git a/include/glaze/concepts/container_concepts.hpp b/include/glaze/concepts/container_concepts.hpp index 7a5fdef7ef..09eaea9a10 100644 --- a/include/glaze/concepts/container_concepts.hpp +++ b/include/glaze/concepts/container_concepts.hpp @@ -8,6 +8,7 @@ #include #include #include +#include // Over time we want most concepts to use the nomenclature: // is_ diff --git a/include/glaze/thread/async_string.hpp b/include/glaze/thread/async_string.hpp index 65680f8c9c..5c6358b027 100644 --- a/include/glaze/thread/async_string.hpp +++ b/include/glaze/thread/async_string.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "glaze/core/common.hpp" @@ -18,9 +19,11 @@ namespace glz { struct async_string { + private: std::string str; mutable std::shared_mutex mutex; + public: async_string() = default; async_string(const char* s) : str(s) {} async_string(const std::string& s) : str(s) {} @@ -98,8 +101,13 @@ namespace glz proxy(std::string& p, std::unique_lock&& lock) noexcept : ptr{&p}, lock(std::move(lock)) {} std::string* operator->() noexcept { return ptr; } + const std::string* operator->() const noexcept { return ptr; } std::string& operator*() noexcept { return *ptr; } + const std::string& operator*() const noexcept { return *ptr; } + + std::string& value() noexcept { return *ptr; } + const std::string& value() const noexcept { return *ptr; } }; proxy write() { return {str, std::unique_lock{mutex}}; } @@ -117,6 +125,8 @@ namespace glz const std::string* operator->() const noexcept { return ptr; } const std::string& operator*() const noexcept { return *ptr; } + + const std::string& value() const noexcept { return *ptr; } }; const_proxy read() const { return {str, std::shared_lock{mutex}}; } @@ -159,17 +169,19 @@ namespace glz str.pop_back(); } - async_string& append(const std::string& s) + template + requires(std::same_as, std::string>) + async_string& append(RHS&& s) { std::unique_lock lock(mutex); - str.append(s); + str.append(std::forward(s)); return *this; } - async_string& append(const char* s) + async_string& append(const char* s, size_t count) { std::unique_lock lock(mutex); - str.append(s); + str.append(s, count); return *this; } @@ -179,10 +191,21 @@ namespace glz str.append(sv); return *this; } + + template + requires(std::same_as, async_string>) + async_string& append(RHS&& other) + { + std::unique_lock lock(mutex, std::defer_lock); + std::shared_lock lock2(other.mutex, std::defer_lock); + std::lock(lock, lock2); + str.append(other.str); + return *this; + } - async_string& operator+=(const std::string& s) { return append(s); } - - async_string& operator+=(const char* s) { return append(s); } + template + requires(std::same_as, std::string>) + async_string& operator+=(RHS&& s) { return append(std::forward(s)); } async_string& operator+=(const std::string_view& sv) { return append(sv); } @@ -192,6 +215,23 @@ namespace glz str += c; return *this; } + + void reserve(size_t count) { + std::unique_lock lock(mutex); + str.reserve(count); + } + + void resize(size_t count) + { + std::unique_lock lock(mutex); + str.resize(count); + } + + void resize(size_t count, const char ch) + { + std::unique_lock lock(mutex); + str.resize(count, ch); + } // Element access char at(size_t pos) const @@ -220,34 +260,52 @@ namespace glz int compare(const async_string& other) const { - std::shared_lock lock1(mutex, std::defer_lock); - std::shared_lock lock2(other.mutex, std::defer_lock); - std::lock(lock1, lock2); + std::scoped_lock lock{mutex, other.mutex}; return str.compare(other.str); } + bool starts_with(const std::string_view other) const + { + std::shared_lock lock{mutex}; + return str.starts_with(other); + } + + bool ends_with(const std::string_view other) const + { + std::shared_lock lock{mutex}; + return str.ends_with(other); + } + + std::string substr(size_t pos = 0, size_t len = std::string::npos) const + { + std::shared_lock lock{mutex}; + return str.substr(pos, len); + } + // Obtain a copy - std::string string() const + friend bool operator==(const async_string& lhs, const std::string_view rhs) { - std::shared_lock lock(mutex); - return str; + std::scoped_lock lock{lhs.mutex}; + return lhs.str == rhs; } - friend bool operator==(const async_string& lhs, const async_string& rhs) + template + requires(std::same_as, async_string>) + friend bool operator==(const async_string& lhs, RHS&& rhs) { - std::shared_lock lock1(lhs.mutex, std::defer_lock); - std::shared_lock lock2(rhs.mutex, std::defer_lock); - std::lock(lock1, lock2); + std::scoped_lock lock{lhs.mutex, rhs.mutex}; return lhs.str == rhs.str; } + + friend bool operator!=(const async_string& lhs, const std::string_view rhs) { return !(lhs == rhs); } - friend bool operator!=(const async_string& lhs, const async_string& rhs) { return !(lhs == rhs); } + template + requires(std::same_as, async_string>) + friend bool operator!=(const async_string& lhs, RHS&& rhs) { return !(lhs == rhs); } friend bool operator<(const async_string& lhs, const async_string& rhs) { - std::shared_lock lock1(lhs.mutex, std::defer_lock); - std::shared_lock lock2(rhs.mutex, std::defer_lock); - std::lock(lock1, lock2); + std::scoped_lock lock{lhs.mutex, rhs.mutex}; return lhs.str < rhs.str; } @@ -260,9 +318,7 @@ namespace glz void swap(async_string& other) { if (this == &other) return; - std::unique_lock lock1(mutex, std::defer_lock); - std::unique_lock lock2(other.mutex, std::defer_lock); - std::lock(lock1, lock2); + std::scoped_lock lock{mutex, other.mutex}; str.swap(other.str); } @@ -279,8 +335,8 @@ namespace glz::detail template static void op(auto&& value, is_context auto&& ctx, auto&& it, auto&& end) noexcept { - std::shared_lock lock{value.mutex}; - read::template op(value.str, ctx, it, end); + auto proxy = value.write(); + read::template op(*proxy, ctx, it, end); } }; @@ -290,8 +346,29 @@ namespace glz::detail template static void op(auto&& value, is_context auto&& ctx, auto&&... args) noexcept { - std::unique_lock lock{value.mutex}; - write::template op(value.str, ctx, args...); + auto proxy = value.read(); + write::template op(*proxy, ctx, args...); + } + }; +} + +// Allow formatting via std::format +#ifdef __cpp_lib_format +#include +namespace std +{ + template <> + struct formatter + { + std::formatter formatter; + + constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) { return formatter.parse(ctx); } + + template + auto format(const glz::async_string& s, FormatContext& ctx) const -> decltype(ctx.out()) + { + return formatter.format(*s.read(), ctx); } }; } +#endif diff --git a/tests/exceptions_test/exceptions_test.cpp b/tests/exceptions_test/exceptions_test.cpp index 69b574fcb8..539120c5e4 100644 --- a/tests/exceptions_test/exceptions_test.cpp +++ b/tests/exceptions_test/exceptions_test.cpp @@ -624,33 +624,33 @@ suite async_string_tests = [] { "async_string param constructors"_test = [] { glz::async_string s1("Hello"); expect(s1.size() == 5) << "s1.size()"; - expect(s1.string() == "Hello"); + expect(s1 == "Hello"); std::string st = "World"; glz::async_string s2(st); - expect(s2.string() == "World"); + expect(s2 == "World"); std::string_view sv("View me"); glz::async_string s3(sv); - expect(s3.string() == "View me"); + expect(s3 == "View me"); // Move construct glz::async_string s4(std::move(s2)); - expect(s4.string() == "World"); + expect(s4 == "World"); expect(s2.empty()); // Moved-from string should be empty }; "async_string copy constructor"_test = [] { glz::async_string original("Copy me"); glz::async_string copy(original); - expect(copy.string() == "Copy me"); + expect(copy == "Copy me"); expect(copy == original); }; "async_string move constructor"_test = [] { glz::async_string original("Move me"); glz::async_string moved(std::move(original)); - expect(moved.string() == "Move me"); + expect(moved == "Move me"); expect(original.empty()); }; @@ -659,31 +659,31 @@ suite async_string_tests = [] { glz::async_string s2("Second"); s1 = s2; expect(s1 == s2); - expect(s1.string() == "Second"); + expect(s1 == "Second"); }; "async_string move assignment"_test = [] { glz::async_string s1("First"); glz::async_string s2("Second"); s1 = std::move(s2); - expect(s1.string() == "Second"); + expect(s1 == "Second"); expect(s2.empty()); }; "async_string assignment from various types"_test = [] { glz::async_string s; s = "Hello again"; - expect(s.string() == "Hello again"); + expect(s == "Hello again"); expect(s.size() == 11); std::string st = "Another test"; s = st; - expect(s.string() == "Another test"); + expect(s == "Another test"); expect(s.size() == 12); std::string_view sv("Testing 123"); s = sv; - expect(s.string() == "Testing 123"); + expect(s == "Testing 123"); expect(s.size() == 11); }; @@ -693,7 +693,7 @@ suite async_string_tests = [] { auto writer = s.write(); writer->append(" data"); } - expect(s.string() == "initial data"); + expect(s == "initial data"); { auto reader = s.read(); @@ -705,11 +705,11 @@ suite async_string_tests = [] { "async_string modifiers"_test = [] { glz::async_string s("Hello"); s.push_back('!'); - expect(s.string() == "Hello!"); + expect(s == "Hello!"); expect(s.size() == 6); s.pop_back(); - expect(s.string() == "Hello"); + expect(s == "Hello"); expect(s.size() == 5); s.clear(); @@ -720,15 +720,15 @@ suite async_string_tests = [] { "async_string append and operator+="_test = [] { glz::async_string s("Hello"); s.append(", ").append("World"); - expect(s.string() == "Hello, World"); + expect(s == "Hello, World"); expect(s.size() == 12); s += "!!!"; - expect(s.string() == "Hello, World!!!"); + expect(s == "Hello, World!!!"); expect(s.size() == 15); s += '?'; - expect(s.string() == "Hello, World!!!?"); + expect(s == "Hello, World!!!?"); expect(s.size() == 16); }; @@ -769,8 +769,8 @@ suite async_string_tests = [] { glz::async_string s1("Hello"); glz::async_string s2("World"); swap(s1, s2); - expect(s1.string() == "World"); - expect(s2.string() == "Hello"); + expect(s1 == "World"); + expect(s2 == "Hello"); }; // Demonstrate Glaze JSON serialization/deserialization @@ -801,6 +801,128 @@ suite async_string_tests = [] { expect(not glz::read_json(t, buffer)); expect(t.empty()); }; + + "async_string starts_with"_test = [] { + glz::async_string s("Hello, World!"); + + // Positive cases + expect(s.starts_with("Hello")); + expect(s.starts_with(std::string("Hello"))); + expect(s.starts_with(std::string_view("Hello"))); + + // Negative cases + expect(not s.starts_with("World")); + expect(not s.starts_with("hello")); // Case-sensitive + expect(not s.starts_with("Hello, World! And more")); + + // Edge cases + glz::async_string empty; + expect(empty.starts_with("")); // An empty string starts with an empty string + expect(not empty.starts_with("Non-empty")); + + expect(s.starts_with("")); // Any string starts with an empty string + }; + + "async_string ends_with"_test = [] { + glz::async_string s("Hello, World!"); + + // Positive cases + expect(s.ends_with("World!")); + expect(s.ends_with(std::string("World!"))); + expect(s.ends_with(std::string_view("World!"))); + + // Negative cases + expect(not s.ends_with("Hello")); + expect(not s.ends_with("world!")); // Case-sensitive + expect(not s.ends_with("...World!")); + + // Edge cases + glz::async_string empty; + expect(empty.ends_with("")); // An empty string ends with an empty string + expect(not empty.ends_with("Non-empty")); + + expect(s.ends_with("")); // Any string ends with an empty string + }; + + "async_string substr"_test = [] { + glz::async_string s("Hello, World!"); + + // Basic substrings + auto sub1 = s.substr(0, 5); + expect(sub1 == "Hello"); + expect(sub1.size() == 5); + + auto sub2 = s.substr(7, 5); + expect(sub2 == "World"); + expect(sub2.size() == 5); + + // Substring to the end + auto sub3 = s.substr(7); + expect(sub3 == "World!"); + expect(sub3.size() == 6); + + // Full string + auto sub4 = s.substr(0, s.size()); + expect(sub4 == s); + + // Empty substring + auto sub5 = s.substr(5, 0); + expect(sub5.empty()); + expect(sub5.size() == 0); + + // Edge cases + glz::async_string empty; + auto sub_empty = empty.substr(0, 1); + expect(sub_empty.empty()); + + // Out of range positions + expect(throws([&] { + s.substr(100, 5); // Start position out of range + })); + + expect(not throws([&] { s.substr(5, 100); })); + + // Start position at the end of the string + auto sub_end = s.substr(s.size(), 0); + expect(sub_end.empty()); + + // Start position just before the end + auto sub_last = s.substr(s.size() - 1, 1); + expect(sub_last == "!"); + expect(sub_last.size() == 1); + }; + +#ifdef __cpp_lib_format + "async_string std::format single argument"_test = [] { + glz::async_string name("Alice"); + std::string formatted = std::format("Hello, {}!", name); + expect(formatted == "Hello, Alice!"); + }; + + "async_string std::format multiple arguments"_test = [] { + glz::async_string name("Bob"); + glz::async_string city("New York"); + std::string formatted = std::format("{} is from {}.", name, city); + expect(formatted == "Bob is from New York."); + }; + + "async_string std::format with empty strings"_test = [] { + glz::async_string empty{}; + + // Formatting with an empty glz::async_string as an argument + std::string formatted_empty_arg = std::format("Hello, {}!", empty); + expect(formatted_empty_arg == "Hello, !"); + }; + + "async_string std::format numeric and other types"_test = [] { + glz::async_string name("Diana"); + int age = 30; + double height = 5.6; + + std::string formatted = std::format("{} is {} years old and {} feet tall.", name, age, height); + expect(formatted == "Diana is 30 years old and 5.6 feet tall."); + }; +#endif }; int main() { return 0; }