diff --git a/src/Corrade/Containers/String.cpp b/src/Corrade/Containers/String.cpp index eaef53f5b..32f6048a9 100644 --- a/src/Corrade/Containers/String.cpp +++ b/src/Corrade/Containers/String.cpp @@ -45,28 +45,42 @@ namespace { static_assert(std::size_t(LargeSizeMask) == Implementation::StringViewSizeMask, "reserved bits should be the same in String and StringView"); +static_assert(std::size_t(LargeSizeMask) == (std::size_t(StringViewFlag::Global)|(std::size_t(Implementation::SmallStringBit) << (sizeof(std::size_t) - 1)*8)), + "small string and global view bits should cover both reserved bits"); String String::nullTerminatedView(StringView view) { - if(view.flags() & StringViewFlag::NullTerminated) - return String{view.data(), view.size(), [](char*, std::size_t){}}; + if(view.flags() & StringViewFlag::NullTerminated) { + String out{view.data(), view.size(), [](char*, std::size_t){}}; + out._large.size |= std::size_t(view.flags() & StringViewFlag::Global); + return out; + } return String{view}; } String String::nullTerminatedView(AllocatedInitT, StringView view) { - if(view.flags() & StringViewFlag::NullTerminated) - return String{view.data(), view.size(), [](char*, std::size_t){}}; + if(view.flags() & StringViewFlag::NullTerminated) { + String out{view.data(), view.size(), [](char*, std::size_t){}}; + out._large.size |= std::size_t(view.flags() & StringViewFlag::Global); + return out; + } return String{AllocatedInit, view}; } String String::nullTerminatedGlobalView(StringView view) { - if(view.flags() >= (StringViewFlag::NullTerminated|StringViewFlag::Global)) - return String{view.data(), view.size(), [](char*, std::size_t){}}; + if(view.flags() >= (StringViewFlag::NullTerminated|StringViewFlag::Global)) { + String out{view.data(), view.size(), [](char*, std::size_t){}}; + out._large.size |= std::size_t(StringViewFlag::Global); + return out; + } return String{view}; } String String::nullTerminatedGlobalView(AllocatedInitT, StringView view) { - if(view.flags() >= (StringViewFlag::NullTerminated|StringViewFlag::Global)) - return String{view.data(), view.size(), [](char*, std::size_t){}}; + if(view.flags() >= (StringViewFlag::NullTerminated|StringViewFlag::Global)) { + String out{view.data(), view.size(), [](char*, std::size_t){}}; + out._large.size |= std::size_t(StringViewFlag::Global); + return out; + } return String{AllocatedInit, view}; } @@ -102,7 +116,13 @@ inline void String::construct(const char* const data, const std::size_t size) { inline void String::destruct() { /* If not SSO, delete the data */ if(_small.size & Implementation::SmallStringBit) return; - if(_large.deleter) _large.deleter(_large.data, _large.size); + /* Instances created with a custom deleter either don't the Global bit set + at all, or have it set but the deleter is a no-op passed from + nullTerminatedView() / nullTerminatedGlobalView(). Thus *technically* + it's not needed to clear the LargeSizeMask (which implies there's also + no way to test that it got cleared), but do it for consistency. */ + if(_large.deleter) + _large.deleter(_large.data, _large.size & ~LargeSizeMask); else delete[] _large.data; } @@ -192,7 +212,7 @@ String::String(AllocatedInitT, String&& other) { /* Otherwise take over the data */ } else { _large.data = other._large.data; - _large.size = other._large.size; + _large.size = other._large.size; /* including the potential Global bit */ _large.deleter = other._large.deleter; } @@ -297,7 +317,7 @@ String::String(String&& other) noexcept { SSO, as for small string we would be doing a copy of _small.data and then also a copy of _small.size *including* the two highest bits */ _large.data = other._large.data; - _large.size = other._large.size; + _large.size = other._large.size; /* including the potential Global bit */ _large.deleter = other._large.deleter; other._large.data = nullptr; other._large.size = 0; @@ -327,7 +347,7 @@ String& String::operator=(String&& other) noexcept { deleting anything. */ using std::swap; swap(other._large.data, _large.data); - swap(other._large.size, _large.size); + swap(other._large.size, _large.size); /* including the potential Global bit */ swap(other._large.deleter, _large.deleter); return *this; } @@ -366,7 +386,7 @@ String::operator Array() && { out = Array{out.release(), size}; std::memcpy(out.data(), _small.data, size); } else { - out = Array{_large.data, _large.size, deleter()}; + out = Array{_large.data, _large.size & ~LargeSizeMask, deleter()}; } /* Same as in release(). Create a zero-size small string to fullfil the @@ -383,7 +403,11 @@ String::operator bool() const { /* The data pointer is guaranteed to be non-null, so no need to check it */ if(_small.size & Implementation::SmallStringBit) return _small.size & ~SmallSizeMask; - return _large.size; + return _large.size & ~LargeSizeMask; +} + +StringViewFlags String::viewFlags() const { + return StringViewFlag(_large.size & std::size_t(StringViewFlag::Global))|StringViewFlag::NullTerminated; } const char* String::data() const { @@ -401,7 +425,7 @@ char* String::data() { bool String::isEmpty() const { if(_small.size & Implementation::SmallStringBit) return !(_small.size & ~SmallSizeMask); - return !_large.size; + return !(_large.size & ~LargeSizeMask); } auto String::deleter() const -> Deleter { @@ -414,7 +438,7 @@ auto String::deleter() const -> Deleter { std::size_t String::size() const { if(_small.size & Implementation::SmallStringBit) return _small.size & ~SmallSizeMask; - return _large.size; + return _large.size & ~LargeSizeMask; } char* String::begin() { @@ -438,19 +462,19 @@ const char* String::cbegin() const { char* String::end() { if(_small.size & Implementation::SmallStringBit) return _small.data + (_small.size & ~SmallSizeMask); - return _large.data + _large.size; + return _large.data + (_large.size & ~LargeSizeMask); } const char* String::end() const { if(_small.size & Implementation::SmallStringBit) return _small.data + (_small.size & ~SmallSizeMask); - return _large.data + _large.size; + return _large.data + (_large.size & ~LargeSizeMask); } const char* String::cend() const { if(_small.size & Implementation::SmallStringBit) return _small.data + (_small.size & ~SmallSizeMask); - return _large.data + _large.size; + return _large.data + (_large.size & ~LargeSizeMask); } /** @todo does it make a practical sense (debug perf) to rewrite these two diff --git a/src/Corrade/Containers/String.h b/src/Corrade/Containers/String.h index 06a01372a..6dc38a5bc 100644 --- a/src/Corrade/Containers/String.h +++ b/src/Corrade/Containers/String.h @@ -46,7 +46,7 @@ namespace Implementation { template struct StringConverter; enum: std::size_t { - SmallStringBit = 0x80, + SmallStringBit = 0x40, SmallStringSize = sizeof(std::size_t)*3 - 1 }; } @@ -261,8 +261,9 @@ class CORRADE_UTILITY_EXPORT String { * * If the view is @ref StringViewFlag::NullTerminated, returns a * non-owning reference to it without any extra allocations or copies - * involved. Otherwise creates a null-terminated owning copy using - * @ref String(StringView). + * involved, propagating also @ref StringViewFlag::Global to + * @ref viewFlags() if present. Otherwise creates a null-terminated + * owning copy using @ref String(StringView). * * This function is primarily meant for efficiently passing * @ref BasicStringView "StringView" instances to APIs that expect @@ -290,7 +291,8 @@ class CORRADE_UTILITY_EXPORT String { * * If the view is both @ref StringViewFlag::NullTerminated and * @ref StringViewFlag::Global, returns a non-owning reference to it - * without any extra allocations or copies involved. Otherwise creates + * without any extra allocations or copies involved, propagating also + * @ref StringViewFlag::Global to @ref viewFlags(). Otherwise creates * a null-terminated owning copy using @ref String(StringView). * * This function is primarily meant for efficiently storing @@ -636,6 +638,17 @@ class CORRADE_UTILITY_EXPORT String { return _small.size & Implementation::SmallStringBit; } + /** + * @brief View flags + * + * A @ref BasicStringView "StringView" constructed from this instance + * will have these flags. @ref StringViewFlag::NullTerminated is + * present always, @ref StringViewFlag::Global if the string was + * originally created from a global null-terminated view with + * @ref nullTerminatedView() or @ref nullTerminatedGlobalView(). + */ + StringViewFlags viewFlags() const; + /** * @brief String data * @@ -1216,12 +1229,12 @@ class CORRADE_UTILITY_EXPORT String { /* Small string optimization. Following size restrictions from StringView (which uses the top two bits for marking global and - null-terminated views), we can use the highest bit to denote a small - string. The second highest bit is currently unused, as it wouldn't - make sense to allow a String to be larger than StringView, since - these are two mutually convertible and interchangeable in many - cases. In case of a large string, there's size, data pointer and - deleter pointer, either 24 (or 12) bytes in total. In case of a + null-terminated views), we can use the second highest bit of the + size to denote a small string. The highest bit, marked as G in the + below diagram, is used to preserve StringViewFlag::Global in case of + a nullTerminatedGlobalView() and a subsequent conversion back to a + StringView. In case of a large string, there's size, data pointer + and deleter pointer, either 24 (or 12) bytes in total. In case of a small string, we can store the size only in one byte out of 8 (or 4), which then gives us 23 (or 11) bytes for storing the actual data, excluding the null terminator that's at most 22 / 10 ASCII @@ -1232,13 +1245,13 @@ class CORRADE_UTILITY_EXPORT String { clarity as well): +-------------------------------+---------+ - | string | si | 01 | + | string | si | 1G | | data | ze | | | 23B/11B | 6b | 2b | +-------------------+-----+++++++---------+ | LSB ||||||| MSB | +---------+---------+-----+++++++---------+ - | data | data | size | 00 | + | data | data | size | 0G | | pointer | deleter | | | | 8B/4B | 8B/4B | 56b/24b | 6b | 2b | +---------+---------+-----------+---------+ @@ -1246,13 +1259,13 @@ class CORRADE_UTILITY_EXPORT String { On BE it's like this: +---------+-------------------------------+ - | 10 | si | string | + | G1 | si | string | | | ze | data | | 2b | 6b | 23B/11B | +---------+++++++-----+-------------------+ | MSB ||||||| LSB | +---------+++++++-----+---------+---------+ - | 00 | size | data | data | + | G0 | size | data | data | | | | pointer | deleter | | 2b | 6b | 56b/24b | 8B/4B | 8B/4B | +---------+-----------+---------+---------+ diff --git a/src/Corrade/Containers/StringView.cpp b/src/Corrade/Containers/StringView.cpp index 838cb8ae8..683de2d7b 100644 --- a/src/Corrade/Containers/StringView.cpp +++ b/src/Corrade/Containers/StringView.cpp @@ -57,11 +57,11 @@ template BasicStringView::BasicStringView(T* const data, const Strin data ? std::strlen(data) : 0, flags|(data ? StringViewFlag::NullTerminated : StringViewFlag::Global)} {} -template BasicStringView::BasicStringView(String& string) noexcept: BasicStringView{string.data(), string.size(), StringViewFlag::NullTerminated} {} +template BasicStringView::BasicStringView(String& string) noexcept: BasicStringView{string.data(), string.size(), string.viewFlags()} {} /* Yes, I'm also surprised this works. On Windows (MSVC, clang-cl and MinGw) it needs an explicit export otherwise the symbol doesn't get exported. */ -template<> template<> CORRADE_UTILITY_EXPORT BasicStringView::BasicStringView(const String& string) noexcept: BasicStringView{string.data(), string.size(), StringViewFlag::NullTerminated} {} +template<> template<> CORRADE_UTILITY_EXPORT BasicStringView::BasicStringView(const String& string) noexcept: BasicStringView{string.data(), string.size(), string.viewFlags()} {} template Array> BasicStringView::split(const char delimiter) const { Array> parts; diff --git a/src/Corrade/Containers/StringView.h b/src/Corrade/Containers/StringView.h index a6585bf97..e1d1211fe 100644 --- a/src/Corrade/Containers/StringView.h +++ b/src/Corrade/Containers/StringView.h @@ -350,7 +350,11 @@ BasicStringView { /** * @brief Construct from a @ref String * - * The resulting view has @ref StringViewFlag::NullTerminated set. + * The resulting view has @ref StringViewFlag::NullTerminated set + * always, and @ref StringViewFlag::Global if the string was originally + * created from a global null-terminated view with + * @ref String::nullTerminatedView() or + * @ref String::nullTerminatedGlobalView(). */ /*implicit*/ BasicStringView(String& data) noexcept; @@ -358,7 +362,10 @@ BasicStringView { * @brief Construct from a const @ref String * * Enabled only if the view is not mutable. The resulting view has - * @ref StringViewFlag::NullTerminated set. + * @ref StringViewFlag::NullTerminated set always, and + * @ref StringViewFlag::Global if the string was created from a global + * null-terminated view with @ref String::nullTerminatedView() or + * @ref String::nullTerminatedGlobalView(). */ template::value>::type> /*implicit*/ BasicStringView(const String& data) noexcept; diff --git a/src/Corrade/Containers/Test/StringTest.cpp b/src/Corrade/Containers/Test/StringTest.cpp index d6de46f86..45e2d5bef 100644 --- a/src/Corrade/Containers/Test/StringTest.cpp +++ b/src/Corrade/Containers/Test/StringTest.cpp @@ -117,6 +117,8 @@ struct StringTest: TestSuite::Tester { void convertMutableStringView(); void convertMutableStringViewSmall(); void convertMutableStringViewSmallAllocatedInit(); + void convertStringViewNullTerminatedGlobalView(); + void convertArrayView(); void convertArrayViewSmall(); void convertArrayViewSmallAllocatedInit(); @@ -127,6 +129,7 @@ struct StringTest: TestSuite::Tester { void convertArraySmall(); void convertArraySmallAllocatedInit(); void convertArrayCustomDeleter(); + void convertArrayNullTerminatedGlobalView(); void convertExternal(); void compare(); @@ -143,15 +146,20 @@ struct StringTest: TestSuite::Tester { void copySmallToSmall(); void moveConstructLarge(); + void moveConstructLargeNullTerminatedGlobalView(); void moveConstructLargeAllocatedInit(); + void moveConstructLargeAllocatedInitNullTerminatedGlobalView(); void moveLargeToLarge(); + void moveLargeToLargeNullTerminatedGlobalView(); void moveLargeToSmall(); + void moveLargeToSmallNullTerminatedGlobalView(); void moveConstructSmall(); void moveConstructSmallAllocatedInit(); void moveSmallToLarge(); void moveSmallToSmall(); void access(); + void accessNullTerminatedGlobalView(); void accessSmall(); void accessInvalid(); @@ -254,6 +262,8 @@ StringTest::StringTest() { &StringTest::convertMutableStringView, &StringTest::convertMutableStringViewSmall, &StringTest::convertMutableStringViewSmallAllocatedInit, + &StringTest::convertStringViewNullTerminatedGlobalView, + &StringTest::convertArrayView, &StringTest::convertArrayViewSmall, &StringTest::convertArrayViewSmallAllocatedInit, @@ -264,6 +274,7 @@ StringTest::StringTest() { &StringTest::convertArraySmall, &StringTest::convertArraySmallAllocatedInit, &StringTest::convertArrayCustomDeleter, + &StringTest::convertArrayNullTerminatedGlobalView, &StringTest::convertExternal, &StringTest::compare, @@ -280,15 +291,20 @@ StringTest::StringTest() { &StringTest::copySmallToSmall, &StringTest::moveConstructLarge, + &StringTest::moveConstructLargeNullTerminatedGlobalView, &StringTest::moveConstructLargeAllocatedInit, + &StringTest::moveConstructLargeAllocatedInitNullTerminatedGlobalView, &StringTest::moveLargeToLarge, + &StringTest::moveLargeToLargeNullTerminatedGlobalView, &StringTest::moveLargeToSmall, + &StringTest::moveLargeToSmallNullTerminatedGlobalView, &StringTest::moveConstructSmall, &StringTest::moveConstructSmallAllocatedInit, &StringTest::moveSmallToLarge, &StringTest::moveSmallToSmall, &StringTest::access, + &StringTest::accessNullTerminatedGlobalView, &StringTest::accessSmall, &StringTest::accessInvalid, @@ -381,6 +397,7 @@ void StringTest::constructDefault() { String a; CORRADE_VERIFY(!a); CORRADE_VERIFY(a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(a.isEmpty()); CORRADE_COMPARE(a.size(), 0); CORRADE_VERIFY(a.data()); @@ -400,6 +417,7 @@ void StringTest::constructTakeOwnership() { }}; CORRADE_VERIFY(a); CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), sizeof(data) - 1); CORRADE_COMPARE(static_cast(a.data()), data); @@ -421,6 +439,7 @@ void StringTest::constructTakeOwnershipConst() { String a{data, 12, [](char*, std::size_t) {}}; CORRADE_VERIFY(a); CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 12); CORRADE_COMPARE(static_cast(a.data()), data); @@ -444,6 +463,7 @@ void StringTest::constructTakeOwnershipImplicitSize() { }}; CORRADE_VERIFY(a); CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 5); CORRADE_COMPARE(static_cast(a.data()), data); @@ -502,6 +522,7 @@ void StringTest::constructPointer() { String a = "Allocated hello for a verbose world\0that rules"; CORRADE_VERIFY(a); CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 35); CORRADE_COMPARE(a.data()[0], 'A'); @@ -514,6 +535,7 @@ void StringTest::constructPointerSmall() { String a = "hello\0world!"; CORRADE_VERIFY(a); CORRADE_VERIFY(a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 5); CORRADE_COMPARE(a.data()[0], 'h'); @@ -529,6 +551,7 @@ void StringTest::constructPointerSmallAllocatedInit() { String a{AllocatedInit, "hello\0world!"}; CORRADE_VERIFY(a); CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 5); CORRADE_COMPARE(a.data()[0], 'h'); @@ -540,6 +563,7 @@ void StringTest::constructPointerNull() { String a = nullptr; CORRADE_VERIFY(!a); CORRADE_VERIFY(a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(a.isEmpty()); CORRADE_COMPARE(a.size(), 0); CORRADE_COMPARE(a.data()[0], '\0'); @@ -549,6 +573,7 @@ void StringTest::constructPointerNullAllocatedInit() { String a{AllocatedInit, nullptr}; CORRADE_VERIFY(!a); CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(a.isEmpty()); CORRADE_COMPARE(a.size(), 0); CORRADE_COMPARE(a.data()[0], '\0'); @@ -559,6 +584,7 @@ void StringTest::constructPointerSize() { String a{"Allocated hello\0for a verbose world\0that rules", 35}; CORRADE_VERIFY(a); CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 35); CORRADE_COMPARE(a.data()[0], 'A'); @@ -570,6 +596,7 @@ void StringTest::constructPointerSizeZero() { String a{"Allocated hello for a verbose world", 0}; CORRADE_VERIFY(!a); CORRADE_VERIFY(a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(a.isEmpty()); CORRADE_COMPARE(a.size(), 0); CORRADE_COMPARE(a.data()[0], '\0'); @@ -579,6 +606,7 @@ void StringTest::constructPointerSizeSmall() { String a{"this\0world\0is hell", 10}; /* `is hell` doesn't get copied */ CORRADE_VERIFY(a); CORRADE_VERIFY(a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 10); CORRADE_COMPARE(a.data()[0], 't'); @@ -590,6 +618,7 @@ void StringTest::constructPointerSizeSmallAllocatedInit() { String a{AllocatedInit, "this\0world\0is hell", 10}; CORRADE_VERIFY(a); CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 10); CORRADE_COMPARE(a.data()[0], 't'); @@ -601,6 +630,7 @@ void StringTest::constructPointerSizeNullZero() { String a{nullptr, 0}; CORRADE_VERIFY(!a); CORRADE_VERIFY(a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(a.isEmpty()); CORRADE_COMPARE(a.size(), 0); CORRADE_COMPARE(a.data()[0], '\0'); @@ -610,6 +640,7 @@ void StringTest::constructPointerSizeNullZeroAllocatedInit() { String a{AllocatedInit, nullptr, 0}; CORRADE_VERIFY(!a); CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(a.isEmpty()); CORRADE_COMPARE(a.size(), 0); CORRADE_COMPARE(a.data()[0], '\0'); @@ -649,6 +680,7 @@ void StringTest::constructValueInit() { String a{Corrade::ValueInit, 35}; CORRADE_VERIFY(a); CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 35); CORRADE_COMPARE(a.data()[0], '\0'); @@ -660,6 +692,7 @@ void StringTest::constructValueInitSmall() { String a{Corrade::ValueInit, 10}; CORRADE_VERIFY(a); CORRADE_VERIFY(a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 10); CORRADE_COMPARE(a.data()[0], '\0'); @@ -675,6 +708,7 @@ void StringTest::constructValueInitZeroSize() { String a{Corrade::ValueInit, 0}; CORRADE_VERIFY(!a); CORRADE_VERIFY(a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(a.isEmpty()); CORRADE_COMPARE(a.size(), 0); CORRADE_COMPARE(a.data()[0], '\0'); @@ -703,6 +737,7 @@ void StringTest::constructDirectInit() { String a{Corrade::DirectInit, 35, 'X'}; CORRADE_VERIFY(a); CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 35); CORRADE_COMPARE(a.data()[0], 'X'); @@ -714,6 +749,7 @@ void StringTest::constructDirectInitSmall() { String a{Corrade::DirectInit, 10, 'X'}; CORRADE_VERIFY(a); CORRADE_VERIFY(a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 10); CORRADE_COMPARE(a.data()[0], 'X'); @@ -729,6 +765,7 @@ void StringTest::constructDirectInitZeroSize() { String a{Corrade::DirectInit, 0, 'X'}; CORRADE_VERIFY(!a); CORRADE_VERIFY(a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(a.isEmpty()); CORRADE_COMPARE(a.size(), 0); CORRADE_COMPARE(a.data()[0], '\0'); @@ -757,6 +794,7 @@ void StringTest::constructNoInit() { String a{Corrade::NoInit, 35}; CORRADE_VERIFY(a); CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 35); /* Contents can be just anything */ @@ -767,6 +805,7 @@ void StringTest::constructNoInitSmall() { String a{Corrade::NoInit, 10}; CORRADE_VERIFY(a); CORRADE_VERIFY(a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(!a.isEmpty()); CORRADE_COMPARE(a.size(), 10); /* Contents can be just anything */ @@ -781,6 +820,7 @@ void StringTest::constructNoInitZeroSize() { String a{Corrade::ValueInit, 0}; CORRADE_VERIFY(!a); CORRADE_VERIFY(a.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(a.isEmpty()); CORRADE_COMPARE(a.size(), 0); CORRADE_COMPARE(a.data()[0], '\0'); @@ -817,6 +857,8 @@ void StringTest::constructNullTerminatedGlobalView() { CORRADE_COMPARE(a, local); CORRADE_VERIFY(a.isSmall()); CORRADE_VERIFY(b.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated); CORRADE_VERIFY(static_cast(a.data()) != local.data()); CORRADE_VERIFY(static_cast(b.data()) != local.data()); } @@ -829,10 +871,12 @@ void StringTest::constructNullTerminatedGlobalView() { String b = String::nullTerminatedGlobalView(localNullTerminated); CORRADE_COMPARE(a, localNullTerminated); CORRADE_COMPARE(b, localNullTerminated); - CORRADE_COMPARE(static_cast(a.data()), localNullTerminated.data()); - CORRADE_VERIFY(static_cast(b.data()) != localNullTerminated.data()); CORRADE_VERIFY(!a.isSmall()); CORRADE_VERIFY(b.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_COMPARE(static_cast(a.data()), localNullTerminated.data()); + CORRADE_VERIFY(static_cast(b.data()) != localNullTerminated.data()); CORRADE_VERIFY(a.deleter()); /* b is small, has no deleter */ } @@ -845,10 +889,14 @@ void StringTest::constructNullTerminatedGlobalView() { String b = String::nullTerminatedGlobalView(globalNullTerminated); CORRADE_COMPARE(a, globalNullTerminated); CORRADE_COMPARE(b, globalNullTerminated); - CORRADE_COMPARE(static_cast(a.data()), globalNullTerminated.data()); - CORRADE_COMPARE(static_cast(b.data()), globalNullTerminated.data()); CORRADE_VERIFY(!a.isSmall()); CORRADE_VERIFY(!b.isSmall()); + /* The Global flag is set even in nullTerminatedView() because why + not */ + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + CORRADE_COMPARE(static_cast(a.data()), globalNullTerminated.data()); + CORRADE_COMPARE(static_cast(b.data()), globalNullTerminated.data()); CORRADE_VERIFY(a.deleter()); CORRADE_VERIFY(b.deleter()); } @@ -861,10 +909,12 @@ void StringTest::constructNullTerminatedGlobalView() { String b = String::nullTerminatedGlobalView(global); CORRADE_COMPARE(a, global); CORRADE_COMPARE(b, global); - CORRADE_VERIFY(static_cast(a.data()) != global.data()); - CORRADE_VERIFY(static_cast(b.data()) != global.data()); CORRADE_VERIFY(a.isSmall()); CORRADE_VERIFY(b.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_VERIFY(static_cast(a.data()) != global.data()); + CORRADE_VERIFY(static_cast(b.data()) != global.data()); /* both a and b is small, has no deleter */ } @@ -879,12 +929,29 @@ void StringTest::constructNullTerminatedGlobalView() { String b = String::nullTerminatedGlobalView(null); CORRADE_COMPARE(a, null); CORRADE_COMPARE(b, null); - CORRADE_VERIFY(static_cast(a.data()) != null.data()); - CORRADE_VERIFY(static_cast(b.data()) != null.data()); CORRADE_VERIFY(a.isSmall()); CORRADE_VERIFY(b.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_VERIFY(static_cast(a.data()) != null.data()); + CORRADE_VERIFY(static_cast(b.data()) != null.data()); /* both a and b is small, has no deleter */ } + + /* Verify that the extra bits are cleared for all accessor APIs */ + for(StringView view: {local, localNullTerminated, global, globalNullTerminated, null}) { + CORRADE_ITERATION(view); + + String a = String::nullTerminatedView(view); + String b = String::nullTerminatedGlobalView(view); + + CORRADE_COMPARE(!!a, !!view); + CORRADE_COMPARE(!!b, !!view); + CORRADE_COMPARE(a.isEmpty(), view.isEmpty()); + CORRADE_COMPARE(b.isEmpty(), view.isEmpty()); + CORRADE_COMPARE(a.size(), view.size()); + CORRADE_COMPARE(b.size(), view.size()); + } } void StringTest::constructNullTerminatedGlobalViewAllocatedInit() { @@ -897,10 +964,12 @@ void StringTest::constructNullTerminatedGlobalViewAllocatedInit() { String b = String::nullTerminatedGlobalView(AllocatedInit, local); CORRADE_COMPARE(a, local); CORRADE_COMPARE(b, local); - CORRADE_VERIFY(static_cast(a.data()) != local.data()); - CORRADE_VERIFY(static_cast(b.data()) != local.data()); CORRADE_VERIFY(!a.isSmall()); CORRADE_VERIFY(!b.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_VERIFY(static_cast(a.data()) != local.data()); + CORRADE_VERIFY(static_cast(b.data()) != local.data()); CORRADE_VERIFY(!a.deleter()); CORRADE_VERIFY(!b.deleter()); } @@ -913,10 +982,12 @@ void StringTest::constructNullTerminatedGlobalViewAllocatedInit() { String b = String::nullTerminatedGlobalView(AllocatedInit, localNullTerminated); CORRADE_COMPARE(a, localNullTerminated); CORRADE_COMPARE(b, localNullTerminated); - CORRADE_COMPARE(static_cast(a.data()), localNullTerminated.data()); - CORRADE_VERIFY(static_cast(b.data()) != localNullTerminated.data()); CORRADE_VERIFY(!a.isSmall()); CORRADE_VERIFY(!b.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_COMPARE(static_cast(a.data()), localNullTerminated.data()); + CORRADE_VERIFY(static_cast(b.data()) != localNullTerminated.data()); CORRADE_VERIFY(a.deleter()); CORRADE_VERIFY(!b.deleter()); } @@ -929,10 +1000,14 @@ void StringTest::constructNullTerminatedGlobalViewAllocatedInit() { String b = String::nullTerminatedGlobalView(AllocatedInit, globalNullTerminated); CORRADE_COMPARE(a, globalNullTerminated); CORRADE_COMPARE(b, globalNullTerminated); - CORRADE_COMPARE(static_cast(a.data()), globalNullTerminated.data()); - CORRADE_COMPARE(static_cast(b.data()), globalNullTerminated.data()); CORRADE_VERIFY(!a.isSmall()); CORRADE_VERIFY(!b.isSmall()); + /* The Global flag is set even in nullTerminatedView() because why + not */ + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + CORRADE_COMPARE(static_cast(a.data()), globalNullTerminated.data()); + CORRADE_COMPARE(static_cast(b.data()), globalNullTerminated.data()); CORRADE_VERIFY(a.deleter()); CORRADE_VERIFY(b.deleter()); } @@ -945,10 +1020,12 @@ void StringTest::constructNullTerminatedGlobalViewAllocatedInit() { String b = String::nullTerminatedGlobalView(AllocatedInit, global); CORRADE_COMPARE(a, global); CORRADE_COMPARE(b, global); - CORRADE_VERIFY(static_cast(a.data()) != global.data()); - CORRADE_VERIFY(static_cast(b.data()) != global.data()); CORRADE_VERIFY(!a.isSmall()); CORRADE_VERIFY(!b.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_VERIFY(static_cast(a.data()) != global.data()); + CORRADE_VERIFY(static_cast(b.data()) != global.data()); CORRADE_VERIFY(!a.deleter()); CORRADE_VERIFY(!b.deleter()); } @@ -964,13 +1041,30 @@ void StringTest::constructNullTerminatedGlobalViewAllocatedInit() { String b = String::nullTerminatedGlobalView(AllocatedInit, null); CORRADE_COMPARE(a, null); CORRADE_COMPARE(b, null); - CORRADE_VERIFY(static_cast(a.data()) != null.data()); - CORRADE_VERIFY(static_cast(b.data()) != null.data()); CORRADE_VERIFY(!a.isSmall()); CORRADE_VERIFY(!b.isSmall()); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated); + CORRADE_VERIFY(static_cast(a.data()) != null.data()); + CORRADE_VERIFY(static_cast(b.data()) != null.data()); CORRADE_VERIFY(!a.deleter()); CORRADE_VERIFY(!b.deleter()); } + + /* Verify that the extra bits are cleared for all accessor APIs */ + for(StringView view: {local, localNullTerminated, global, globalNullTerminated, null}) { + CORRADE_ITERATION(view); + + String a = String::nullTerminatedView(view); + String b = String::nullTerminatedGlobalView(view); + + CORRADE_COMPARE(!!a, !!view); + CORRADE_COMPARE(!!b, !!view); + CORRADE_COMPARE(a.isEmpty(), view.isEmpty()); + CORRADE_COMPARE(b.isEmpty(), view.isEmpty()); + CORRADE_COMPARE(a.size(), view.size()); + CORRADE_COMPARE(b.size(), view.size()); + } } void StringTest::convertStringView() { @@ -1076,6 +1170,26 @@ void StringTest::convertMutableStringViewSmallAllocatedInit() { CORRADE_COMPARE(static_cast(aView.data()), a.data()); } +void StringTest::convertStringViewNullTerminatedGlobalView() { + String a = String::nullTerminatedView("null terminated"_s); + String aAllocated = String::nullTerminatedView(AllocatedInit, "null terminated, allocated"_s); + String aGlobal = String::nullTerminatedGlobalView("null terminated, global"_s); + String aGlobalAllocated = String::nullTerminatedGlobalView(AllocatedInit, "null terminated, global, allocated"_s); + + /* Using pointers to avoid extra complexity from copy/move constructors + potentially discarding the flags */ + for(String* s: {&a, &aAllocated, &aGlobal, &aGlobalAllocated}) { + CORRADE_COMPARE(s->viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + + StringView aView = *s; + MutableStringView aMutableView = *s; + CORRADE_COMPARE(aView, *s); + CORRADE_COMPARE(aMutableView, *s); + CORRADE_COMPARE(aView.flags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + CORRADE_COMPARE(aMutableView.flags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + } +} + void StringTest::convertArrayView() { const String a = arrayView("Allocated hello\0for a verbose world").exceptSuffix(1); CORRADE_VERIFY(a); @@ -1256,6 +1370,17 @@ void StringTest::convertArrayCustomDeleter() { CORRADE_COMPARE(a.data()[0], '\0'); } +void StringTest::convertArrayNullTerminatedGlobalView() { + StringView view = "Allocated hello\0for a verbose world"_s; + String a = String::nullTerminatedGlobalView(view); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + + /* The size should be returned with the Global bit cleared */ + Array array = std::move(a); + CORRADE_COMPARE(array.size(), view.size()); + CORRADE_COMPARE(StringView{array}, view); +} + void StringTest::convertExternal() { Str a{"hello", 5}; @@ -1536,6 +1661,19 @@ void StringTest::moveConstructLarge() { CORRADE_VERIFY(std::is_nothrow_move_constructible::value); } +void StringTest::moveConstructLargeNullTerminatedGlobalView() { + StringView view = "Allocated hello for a verbose world"_s; + + String a = String::nullTerminatedGlobalView(view); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + + /* The Global flag should be preserved */ + String b = Utility::move(a); + CORRADE_COMPARE(b, "Allocated hello for a verbose world"_s); + CORRADE_VERIFY(b.data() == view.data()); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); +} + void StringTest::moveConstructLargeAllocatedInit() { /* Same as above, for already-large strings it should have no difference */ @@ -1561,6 +1699,19 @@ void StringTest::moveConstructLargeAllocatedInit() { CORRADE_VERIFY(std::is_nothrow_move_constructible::value); } +void StringTest::moveConstructLargeAllocatedInitNullTerminatedGlobalView() { + StringView view = "Allocated hello for a verbose world"_s; + + String a = String::nullTerminatedGlobalView(view); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + + /* The Global flag should be preserved */ + String b{AllocatedInit, Utility::move(a)}; + CORRADE_COMPARE(b, "Allocated hello for a verbose world"_s); + CORRADE_VERIFY(b.data() == view.data()); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); +} + void StringTest::moveLargeToLarge() { char aData[] = "Allocated hello for a verbose world"; char bData[] = "ALLOCATED HELLO FOR A VERBOSE WORLD!!!"; @@ -1597,6 +1748,37 @@ void StringTest::moveLargeToLarge() { CORRADE_VERIFY(std::is_nothrow_move_assignable::value); } +void StringTest::moveLargeToLargeNullTerminatedGlobalView() { + StringView view = "Allocated hello for a verbose world"_s; + char bData[] = "ALLOCATED HELLO FOR A VERBOSE WORLD!!!"; + + { + String a = String::nullTerminatedGlobalView(view); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + + String b{bData, sizeof(bData) - 1, [](char* data, std::size_t){ + ++data[1]; + }}; + CORRADE_VERIFY(!b.isSmall()); + CORRADE_VERIFY(b.deleter()); + + /* The two are simply swapped; the Global flag should be preserved in + B */ + b = Utility::move(a); + CORRADE_COMPARE(b, "Allocated hello for a verbose world"_s); + CORRADE_VERIFY(b.data() == view.data()); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + + /* No deleter fired yet */ + CORRADE_COMPARE(bData[1], 'L'); + } + + /* a is deallocated as usual */ + CORRADE_COMPARE(bData[1], 'M'); + + CORRADE_VERIFY(std::is_nothrow_move_assignable::value); +} + void StringTest::moveLargeToSmall() { char aData[] = "Allocated hello for a verbose world"; @@ -1625,6 +1807,22 @@ void StringTest::moveLargeToSmall() { CORRADE_COMPARE(aData[0], 'B'); } +void StringTest::moveLargeToSmallNullTerminatedGlobalView() { + StringView view = "Allocated hello for a verbose world"_s; + + String a = String::nullTerminatedGlobalView(view); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + + String b = "hello"; + CORRADE_VERIFY(b.isSmall()); + + /* The two are simply swapped; the Global flag should be preserved in B */ + b = Utility::move(a); + CORRADE_COMPARE(b, "Allocated hello for a verbose world"_s); + CORRADE_VERIFY(b.data() == view.data()); + CORRADE_COMPARE(b.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); +} + void StringTest::moveConstructSmall() { String a = "hello"; CORRADE_VERIFY(a.isSmall()); @@ -1717,6 +1915,32 @@ void StringTest::access() { CORRADE_COMPARE(ca[14], 'o'); } +void StringTest::accessNullTerminatedGlobalView() { + /* Like access(), but wrapping a view and not modifying any data. + Everything should behave the same. */ + + String a = String::nullTerminatedGlobalView("Allocated hello for a verbose world"_s); + CORRADE_COMPARE(a.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + CORRADE_VERIFY(!a.isSmall()); + CORRADE_COMPARE(*a.begin(), 'A'); + CORRADE_COMPARE(*a.cbegin(), 'A'); + CORRADE_COMPARE(a.front(), 'A'); + CORRADE_COMPARE(*(a.end() - 1), 'd'); + CORRADE_COMPARE(*(a.cend() - 1), 'd'); + CORRADE_COMPARE(a.back(), 'd'); + + const String ca = String::nullTerminatedGlobalView("Allocated hello for a verbose world"_s); + CORRADE_COMPARE(ca.viewFlags(), StringViewFlag::NullTerminated|StringViewFlag::Global); + CORRADE_VERIFY(!ca.isSmall()); + CORRADE_COMPARE(*ca.begin(), 'A'); + CORRADE_COMPARE(*ca.cbegin(), 'A'); + CORRADE_COMPARE(ca.front(), 'A'); + CORRADE_COMPARE(*(ca.end() - 1), 'd'); + CORRADE_COMPARE(*(ca.cend() - 1), 'd'); + CORRADE_COMPARE(ca.back(), 'd'); + CORRADE_COMPARE(ca[14], 'o'); +} + void StringTest::accessSmall() { String a = "hello!"; CORRADE_VERIFY(a.isSmall());