From 10ac88e38b9e0d65d6141135e576ae8e9a6a78dd Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 22 Feb 2025 11:37:39 +0100 Subject: [PATCH] feat(eventsub): use custom `String` type (#5968) --- CHANGELOG.md | 2 +- .../docs/twitch-eventsub-ws.natvis | 33 ++ .../include/twitch-eventsub-ws/string.hpp | 228 +++++++-- lib/twitch-eventsub-ws/src/CMakeLists.txt | 5 + lib/twitch-eventsub-ws/src/string.cpp | 15 +- lib/twitch-eventsub-ws/tests/CMakeLists.txt | 1 + lib/twitch-eventsub-ws/tests/src/string.cpp | 439 ++++++++++++++++++ 7 files changed, 681 insertions(+), 42 deletions(-) create mode 100644 lib/twitch-eventsub-ws/docs/twitch-eventsub-ws.natvis create mode 100644 lib/twitch-eventsub-ws/tests/src/string.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index babb653f..50f1976a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ - Bugfix: Fixed channel point redemptions with messages not showing up if PubSub is disconnected. (#5948) - Bugfix: Fixed the input font not immediately updating when zooming in/out. (#5960) - Dev: Subscriptions to PubSub channel points redemption topics now use no auth token, making it continue to work during PubSub shutdown. (#5947) -- Dev: Add initial experimental EventSub support. (#5837, #5895, #5897, #5904, #5910, #5903, #5915, #5916, #5930, #5935, #5932, #5943, #5952, #5953) +- Dev: Add initial experimental EventSub support. (#5837, #5895, #5897, #5904, #5910, #5903, #5915, #5916, #5930, #5935, #5932, #5943, #5952, #5953, #5968) - Dev: Remove unneeded platform specifier for toasts. (#5914) - Dev: Highlight checks now use non-capturing groups for the boundaries. (#5784) - Dev: Removed unused PubSub whisper code. (#5898) diff --git a/lib/twitch-eventsub-ws/docs/twitch-eventsub-ws.natvis b/lib/twitch-eventsub-ws/docs/twitch-eventsub-ws.natvis new file mode 100644 index 00000000..3e51b5e3 --- /dev/null +++ b/lib/twitch-eventsub-ws/docs/twitch-eventsub-ws.natvis @@ -0,0 +1,33 @@ + + + + + + + + + {storage.inPlace,[size()]s8} + {storage.data,[size()]s8} + {storage.qt.d.ptr,su} + + + + InPlace + Allocated + QString + + + + size() + + storage.inPlace,[size()] + + + storage.data,[size()] + + + storage.qt + + + + diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/string.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/string.hpp index d9178ed2..674047ad 100644 --- a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/string.hpp +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/string.hpp @@ -1,67 +1,227 @@ #pragma once #include +#include #include -#include -#include +#include +// NOLINTBEGIN(cppcoreguidelines-pro-type-union-access) +// NOLINTBEGIN(bugprone-undefined-memory-manipulation) +// NOLINTBEGIN(cppcoreguidelines-avoid-c-arrays) namespace chatterino::eventsub::lib { -template -constexpr bool DEPENDENT_FALSE = false; // workaround before CWG2518/P2593R1 - -/// String is a struct that holds either an std::string or a QString +/// String is a struct that holds either an UTF-8 string or a QString /// -/// The intended use is for it to receive an std::string that has been built by +/// The intended use is for it to receive an UTF-8 string that has been built by /// boost::json, and once it's been passed into a GUI appliciaton, it can use /// the `qt` function to convert the backing string to a QString, /// while we ensure the conversion is only done once. +/// The string can always be viewed using \ref view() in Qt6. +/// +/// It uses the small-string-optimization and has `sizeof(QString)` bytes of +/// storage it can use without allocating (Qt6: 24B, Qt5: 8B). The stored string +/// is **not** null-terminated. struct String { - explicit String(std::string &&v) - : backingString(std::move(v)) + constexpr String() noexcept = default; + constexpr String(std::string_view sv) + : flags(sv.length() & LENGTH_MASK) { + if (sv.length() <= SSO_CAPACITY) + { + std::memcpy(this->storage.inPlace, sv.data(), sv.length()); + return; + } + + this->flags |= ALLOC_BIT; + auto *data = new char[sv.length()]; + std::memcpy(data, sv.data(), sv.length()); + this->storage.data = data; } - ~String() = default; + ~String() + { + if (this->isQt()) + { + this->storage.qt.~QString(); + } + else if (this->isAlloc()) + { + delete[] this->storage.data; + } + // else: inPlace + } String(const String &s) = delete; - String(String &&s) = default; - String &operator=(const String &) = delete; - String &operator=(String &&) = default; + + String(String &&other) noexcept + : storage(std::move(other.storage)) + , flags(other.flags) + { + other.flags = 0; + } + + String &operator=(String &&other) noexcept + { + // clear our data (if needed) + if (this->isQt()) + { + this->storage.qt.~QString(); + } + else if (this->isAlloc()) + { + delete[] this->storage.data; + } + + this->storage = std::move(other.storage); + this->flags = other.flags; + other.flags = 0; + + return *this; + } /// Returns the string as a QString, modifying the backing string to ensure /// the copy only happens once. QString qt() const { - return std::visit( - [this](auto &&arg) -> QString { - using T = std::decay_t>; - if constexpr (std::is_same_v) - { - auto qtString = QString::fromStdString(arg); - this->backingString = qtString; - return qtString; - } - else if constexpr (std::is_same_v) - { - return arg; - } - else - { - static_assert(DEPENDENT_FALSE, - "unknown type in variant"); - } - }, - this->backingString); + if (this->isEmpty()) + { + return {}; + } + + if (this->isQt()) + { + return this->storage.qt; + } + + // not a QString yet + if (this->isAlloc()) + { + auto s = QString::fromUtf8( + this->storage.data, + static_cast(this->flags & LENGTH_MASK)); + delete[] this->storage.data; + new (&this->storage.qt) QString(std::move(s)); + this->flags &= ~ALLOC_BIT; + } + else + { + auto s = QString::fromUtf8( + this->storage.inPlace, + static_cast(this->flags & LENGTH_MASK)); + new (&this->storage.qt) QString(std::move(s)); + } + this->flags |= QT_BIT; + + return this->storage.qt; + } + + QAnyStringView view() const noexcept + { + if (this->isQt()) + { + return this->storage.qt; + } + return { + this->isAlloc() ? this->storage.data : this->storage.inPlace, + static_cast(this->flags & LENGTH_MASK), + }; + } + + constexpr bool isEmpty() const noexcept + { + return this->flags == 0; + } + + constexpr bool isQt() const noexcept + { + return (this->flags & QT_BIT) != 0; + } + + constexpr bool isAlloc() const noexcept + { + return (this->flags & ALLOC_BIT) != 0; + } + + constexpr bool isInPlace() const noexcept + { + return (this->flags & (ALLOC_BIT | QT_BIT)) == 0; } private: - mutable std::variant backingString; + static constexpr size_t QT_BIT = 1ULL << (sizeof(size_t) * 8 - 1); + static constexpr size_t ALLOC_BIT = 1ULL << (sizeof(size_t) * 8 - 2); + static constexpr size_t LENGTH_MASK = std::min(ALLOC_BIT, QT_BIT) - 1; + static constexpr size_t SSO_CAPACITY = sizeof(QString); + + static_assert((LENGTH_MASK & ALLOC_BIT) == 0); + static_assert((LENGTH_MASK & QT_BIT) == 0); + + constexpr size_t length() const noexcept + { + if ((this->flags & QT_BIT) != 0) + { + return this->storage.qt.length(); + } + return this->flags & LENGTH_MASK; + } + + mutable union Storage { + constexpr Storage() noexcept + { + } + ~Storage() noexcept + { + } + + Storage(const Storage &) = delete; + Storage &operator=(const Storage &) = delete; + + // we can memcpy QStrings as they're relocatable + static_assert(QTypeInfo::isRelocatable != 0); + + Storage(Storage &&other) noexcept + { + Storage::move(std::addressof(other), this); + }; + Storage &operator=(Storage &&other) noexcept + { + Storage::move(std::addressof(other), this); + return *this; + }; + + char inPlace[SSO_CAPACITY]{}; + const char *data; + QString qt; + + private: + static void move(Storage *from, Storage *to) + { + // copy `from` -> `to` + std::memcpy(to, from, sizeof(Storage)); + // clear `from` + std::memset(from, 0, sizeof(Storage)); + } + } storage; + static_assert(sizeof(Storage) == sizeof(QString)); + + /// Flags both store the length as well as the current state of the string. + /// They're defined as follows (big endian, MSB is first, 64bit): + /// ``` + /// ┌──────┬──────┬─────────┬─────────────────────────────────────────┐ + /// │Bit(s)│ 63 │ 62 │ 61..0 │ + /// ├──────┼──────┼─────────┼─────────────────────────────────────────┤ + /// │Value │ isQt │ isAlloc │ length │ + /// └──────┴──────┴─────────┴─────────────────────────────────────────┘ + /// ``` + mutable size_t flags = 0; }; boost::json::result_for::type tag_invoke( boost::json::try_value_to_tag, const boost::json::value &jvRoot); } // namespace chatterino::eventsub::lib +// NOLINTEND(cppcoreguidelines-avoid-c-arrays) +// NOLINTEND(bugprone-undefined-memory-manipulation) +// NOLINTEND(cppcoreguidelines-pro-type-union-access) diff --git a/lib/twitch-eventsub-ws/src/CMakeLists.txt b/lib/twitch-eventsub-ws/src/CMakeLists.txt index 094dbc0e..79b9d918 100644 --- a/lib/twitch-eventsub-ws/src/CMakeLists.txt +++ b/lib/twitch-eventsub-ws/src/CMakeLists.txt @@ -71,6 +71,11 @@ if (MSVC) # Use the function inlining level from 'Release' mode (2). string(REPLACE "/Ob1" "/Ob2" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") + # Add nativs to PDB + set(_natvis_path "${CMAKE_CURRENT_LIST_DIR}/../docs/twitch-eventsub-ws.natvis") + cmake_path(ABSOLUTE_PATH _natvis_path NORMALIZE) + target_link_options(${PROJECT_NAME} INTERFACE "/NATVIS:${_natvis_path}") + # Configure warnings # Someone adds /W3 before we add /W4. diff --git a/lib/twitch-eventsub-ws/src/string.cpp b/lib/twitch-eventsub-ws/src/string.cpp index c6b0312d..6dde2378 100644 --- a/lib/twitch-eventsub-ws/src/string.cpp +++ b/lib/twitch-eventsub-ws/src/string.cpp @@ -1,10 +1,10 @@ #include "twitch-eventsub-ws/string.hpp" +#include "twitch-eventsub-ws/errors.hpp" + #include #include -#include - namespace chatterino::eventsub::lib { boost::json::result_for::type tag_invoke( @@ -14,16 +14,17 @@ boost::json::result_for::type tag_invoke( if (jvRoot.is_null()) { // We treat null "strings" as empty strings - return String(""); + return String(); } - auto v = boost::json::try_value_to(jvRoot); - if (v.has_error()) + if (!jvRoot.is_string()) { - return v.error(); + // we don't need a source_location - it doesn't tell us much + return lib::error::makeCode(error::Kind::ExpectedString, nullptr); } - return String(std::move(v.value())); + const auto &str = jvRoot.get_string(); + return String(str); } } // namespace chatterino::eventsub::lib diff --git a/lib/twitch-eventsub-ws/tests/CMakeLists.txt b/lib/twitch-eventsub-ws/tests/CMakeLists.txt index c2289c65..da285a9b 100644 --- a/lib/twitch-eventsub-ws/tests/CMakeLists.txt +++ b/lib/twitch-eventsub-ws/tests/CMakeLists.txt @@ -2,6 +2,7 @@ project(twitch-eventsub-ws-test) set(SOURCES src/parse.cpp + src/string.cpp ) add_executable(${PROJECT_NAME} ${SOURCES}) diff --git a/lib/twitch-eventsub-ws/tests/src/string.cpp b/lib/twitch-eventsub-ws/tests/src/string.cpp new file mode 100644 index 00000000..1f7292f0 --- /dev/null +++ b/lib/twitch-eventsub-ws/tests/src/string.cpp @@ -0,0 +1,439 @@ +#include +#include +#include + +#include + +namespace { + +constexpr const char *LONGEST_SSO = "mylongstringyesitslongwo"; +constexpr const char *TOO_LONG = "mylongstringyesitslongwow"; + +constexpr const char *REALLY_LONG = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi lectus " + "massa, efficitur non elit vel, faucibus pellentesque enim."; + +} // namespace + +using String = chatterino::eventsub::lib::String; + +static_assert(std::is_move_assignable_v && + std::is_move_constructible_v && + !std::is_copy_assignable_v && + !std::is_copy_constructible_v && + !std::is_trivially_copyable_v && + !std::is_trivially_move_assignable_v && + !std::is_trivially_copy_assignable_v && + !std::is_trivially_move_constructible_v && + !std::is_trivially_copy_constructible_v); + +TEST(String, Construction) +{ + { + String s; + ASSERT_TRUE(s.isInPlace()); + ASSERT_FALSE(s.isQt()); + ASSERT_FALSE(s.isAlloc()); + ASSERT_TRUE(s.isEmpty()); + ASSERT_EQ(s.view(), ""); + } + { + String s(""); + ASSERT_TRUE(s.isInPlace()); + ASSERT_FALSE(s.isQt()); + ASSERT_FALSE(s.isAlloc()); + ASSERT_TRUE(s.isEmpty()); + ASSERT_EQ(s.view(), ""); + } + { + String s("foo"); + ASSERT_TRUE(s.isInPlace()); + ASSERT_FALSE(s.isQt()); + ASSERT_FALSE(s.isAlloc()); + ASSERT_FALSE(s.isEmpty()); + ASSERT_EQ(s.view(), "foo"); + } + { + String s(LONGEST_SSO); + ASSERT_TRUE(s.isInPlace()); + ASSERT_FALSE(s.isQt()); + ASSERT_FALSE(s.isAlloc()); + ASSERT_FALSE(s.isEmpty()); + ASSERT_EQ(s.view(), LONGEST_SSO); + } + { + String s(TOO_LONG); + ASSERT_FALSE(s.isInPlace()); + ASSERT_FALSE(s.isQt()); + ASSERT_TRUE(s.isAlloc()); + ASSERT_FALSE(s.isEmpty()); + ASSERT_EQ(s.view(), TOO_LONG); + } +} + +TEST(String, ToQt) +{ + { + String s; + ASSERT_FALSE(s.isQt()); + ASSERT_EQ(s.qt(), ""); + ASSERT_FALSE(s.isQt()); + ASSERT_FALSE(s.isAlloc()); + ASSERT_TRUE(s.isEmpty()); + } + { + String s(""); + ASSERT_FALSE(s.isQt()); + ASSERT_EQ(s.qt(), ""); + ASSERT_FALSE(s.isQt()); + ASSERT_FALSE(s.isAlloc()); + ASSERT_TRUE(s.isEmpty()); + } + { + String s("foo"); + ASSERT_FALSE(s.isQt()); + ASSERT_EQ(s.qt(), "foo"); + ASSERT_TRUE(s.isQt()); + ASSERT_FALSE(s.isAlloc()); + ASSERT_FALSE(s.isEmpty()); + } + { + String s(LONGEST_SSO); + ASSERT_FALSE(s.isQt()); + ASSERT_EQ(s.qt(), LONGEST_SSO); + ASSERT_TRUE(s.isQt()); + ASSERT_FALSE(s.isAlloc()); + ASSERT_FALSE(s.isEmpty()); + } + { + String s(TOO_LONG); + ASSERT_FALSE(s.isQt()); + ASSERT_EQ(s.qt(), TOO_LONG); + ASSERT_TRUE(s.isQt()); + ASSERT_FALSE(s.isAlloc()); + ASSERT_FALSE(s.isEmpty()); + } + { + String s(REALLY_LONG); + ASSERT_FALSE(s.isQt()); + ASSERT_EQ(s.qt(), REALLY_LONG); + ASSERT_TRUE(s.isQt()); + ASSERT_FALSE(s.isAlloc()); + ASSERT_FALSE(s.isEmpty()); + } +} + +TEST(String, MoveCtor) +{ + { + String u; + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + String v(std::move(u)); + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_TRUE(v.isEmpty()); + ASSERT_EQ(v.view(), ""); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u(""); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + String v(std::move(u)); + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_TRUE(v.isEmpty()); + ASSERT_EQ(v.view(), ""); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u("foo"); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + String v(std::move(u)); + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_FALSE(v.isEmpty()); + ASSERT_EQ(v.view(), "foo"); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u(LONGEST_SSO); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + String v(std::move(u)); + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_FALSE(v.isEmpty()); + ASSERT_EQ(v.view(), LONGEST_SSO); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u(TOO_LONG); + ASSERT_FALSE(u.isQt()); + ASSERT_TRUE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + String v(std::move(u)); + ASSERT_FALSE(v.isQt()); + ASSERT_TRUE(v.isAlloc()); + ASSERT_FALSE(v.isEmpty()); + ASSERT_EQ(v.view(), TOO_LONG); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u(LONGEST_SSO); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + ASSERT_EQ(u.qt(), LONGEST_SSO); + ASSERT_TRUE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + String v(std::move(u)); + ASSERT_TRUE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_FALSE(v.isEmpty()); + ASSERT_EQ(v.view(), LONGEST_SSO); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u(TOO_LONG); + ASSERT_FALSE(u.isQt()); + ASSERT_TRUE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + ASSERT_EQ(u.qt(), TOO_LONG); + ASSERT_TRUE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + String v(std::move(u)); + ASSERT_TRUE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_FALSE(v.isEmpty()); + ASSERT_EQ(v.view(), TOO_LONG); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u(REALLY_LONG); + ASSERT_FALSE(u.isQt()); + ASSERT_TRUE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + ASSERT_EQ(u.qt(), REALLY_LONG); + ASSERT_TRUE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + String v(std::move(u)); + ASSERT_TRUE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_FALSE(v.isEmpty()); + ASSERT_EQ(v.view(), REALLY_LONG); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } +} + +TEST(String, MoveAssign) +{ + { + String u; + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + String v; + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_TRUE(v.isEmpty()); + v = std::move(u); + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_TRUE(v.isEmpty()); + ASSERT_EQ(v.view(), ""); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u(""); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + String v; + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_TRUE(v.isEmpty()); + v = std::move(u); + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_TRUE(v.isEmpty()); + ASSERT_EQ(v.view(), ""); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u("foo"); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + String v; + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_TRUE(v.isEmpty()); + v = std::move(u); + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_FALSE(v.isEmpty()); + ASSERT_EQ(v.view(), "foo"); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u(LONGEST_SSO); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + String v; + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_TRUE(v.isEmpty()); + v = std::move(u); + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_FALSE(v.isEmpty()); + ASSERT_EQ(v.view(), LONGEST_SSO); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u(TOO_LONG); + ASSERT_FALSE(u.isQt()); + ASSERT_TRUE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + String v; + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_TRUE(v.isEmpty()); + v = std::move(u); + ASSERT_FALSE(v.isQt()); + ASSERT_TRUE(v.isAlloc()); + ASSERT_FALSE(v.isEmpty()); + ASSERT_EQ(v.view(), TOO_LONG); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u(LONGEST_SSO); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + ASSERT_EQ(u.qt(), LONGEST_SSO); + ASSERT_TRUE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + String v; + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_TRUE(v.isEmpty()); + v = std::move(u); + ASSERT_TRUE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_FALSE(v.isEmpty()); + ASSERT_EQ(v.view(), LONGEST_SSO); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u(TOO_LONG); + ASSERT_FALSE(u.isQt()); + ASSERT_TRUE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + ASSERT_EQ(u.qt(), TOO_LONG); + ASSERT_TRUE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + String v; + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_TRUE(v.isEmpty()); + v = std::move(u); + ASSERT_TRUE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_FALSE(v.isEmpty()); + ASSERT_EQ(v.view(), TOO_LONG); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } + { + String u(REALLY_LONG); + ASSERT_FALSE(u.isQt()); + ASSERT_TRUE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + ASSERT_EQ(u.qt(), REALLY_LONG); + ASSERT_TRUE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_FALSE(u.isEmpty()); + String v; + ASSERT_FALSE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_TRUE(v.isEmpty()); + v = std::move(u); + ASSERT_TRUE(v.isQt()); + ASSERT_FALSE(v.isAlloc()); + ASSERT_FALSE(v.isEmpty()); + ASSERT_EQ(v.view(), REALLY_LONG); + ASSERT_FALSE(u.isQt()); + ASSERT_FALSE(u.isAlloc()); + ASSERT_TRUE(u.isEmpty()); + ASSERT_EQ(u.view(), ""); + } +} + +TEST(String, QtLifetime) +{ + QString qt; + { + String s(REALLY_LONG); + qt = s.qt(); + ASSERT_TRUE(!qt.isDetached()); // s holds the string too + } + ASSERT_TRUE(qt.isDetached()); +}