-
Notifications
You must be signed in to change notification settings - Fork 300
basic_zstring_view: add opt-in nonnull variants #668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ba62fcc
abfa087
9043fbb
4a15119
1770e23
02faec8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -27,6 +27,9 @@ | |||||||||||
| #ifndef WI_STL_FAIL_FAST_IF | ||||||||||||
| #define WI_STL_FAIL_FAST_IF FAIL_FAST_IF | ||||||||||||
| #endif | ||||||||||||
| #ifndef WI_STL_FAIL_FAST_IF_NULL | ||||||||||||
| #define WI_STL_FAIL_FAST_IF_NULL FAIL_FAST_IF_NULL | ||||||||||||
| #endif | ||||||||||||
| /// @endcond | ||||||||||||
|
|
||||||||||||
| #if defined(WIL_ENABLE_EXCEPTIONS) | ||||||||||||
|
|
@@ -160,6 +163,55 @@ inline wil::unique_bstr make_bstr(std::wstring_view source) | |||||||||||
| #endif // WIL_ENABLE_EXCEPTIONS | ||||||||||||
| #endif // defined(__WIL_OLEAUTO_H_) | ||||||||||||
|
|
||||||||||||
| template <class TChar, class Traits = std::char_traits<TChar>> | ||||||||||||
| class basic_zstring_view; | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| Traits policy for a basic_zstring_view whose constructors reject null pointers and whose default constructor points | ||||||||||||
| at an internal empty string. The nested char_traits alias keeps the resulting basic_zstring_view derived from the | ||||||||||||
| same std::basic_string_view specialization as the nullable form. | ||||||||||||
|
|
||||||||||||
| @note basic_zstring_view publicly inherits from std::basic_string_view. A caller can explicitly cast to a mutable | ||||||||||||
| base reference and assign a view with null data, bypassing the policy. Avoid mutating the object through a base | ||||||||||||
|
Comment on lines
+174
to
+175
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The issue is more general than this and is not specific to the nonnull type. E.g. you can assign non-null terminated data to both |
||||||||||||
| reference. | ||||||||||||
| */ | ||||||||||||
| template <typename TChar, typename Traits = std::char_traits<TChar>> | ||||||||||||
| struct nonnull_zstring_view_traits | ||||||||||||
| { | ||||||||||||
| using char_traits = Traits; | ||||||||||||
| static constexpr bool empty_strings_are_non_null = true; | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| namespace details | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing |
||||||||||||
| { | ||||||||||||
| template <typename TChar, typename Traits, typename = void> | ||||||||||||
| struct zstring_view_traits | ||||||||||||
| { | ||||||||||||
| static constexpr bool empty_strings_are_non_null = false; | ||||||||||||
| using char_traits = Traits; | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| template <typename TChar, typename Traits> | ||||||||||||
| struct zstring_view_traits<TChar, Traits, std::void_t<decltype(Traits::empty_strings_are_non_null)>> | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Otherwise this would fail if not provided. |
||||||||||||
| { | ||||||||||||
| static constexpr bool empty_strings_are_non_null = Traits::empty_strings_are_non_null; | ||||||||||||
| using char_traits = typename Traits::char_traits; | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| template <typename T> | ||||||||||||
| struct is_basic_zstring_view : std::false_type | ||||||||||||
| { | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| template <typename TChar, typename Traits> | ||||||||||||
| struct is_basic_zstring_view<basic_zstring_view<TChar, Traits>> : std::true_type | ||||||||||||
| { | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| template <typename TChar> | ||||||||||||
| inline constexpr TChar zstring_view_empty_storage[1]{TChar()}; | ||||||||||||
| } // namespace details | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| zstring_view. A zstring_view is identical to a std::string_view except it is always nul-terminated (unless empty). | ||||||||||||
| * zstring_view can be used for storing string literals without "forgetting" the length or that it is nul-terminated. | ||||||||||||
|
|
@@ -169,11 +221,18 @@ inline wil::unique_bstr make_bstr(std::wstring_view source) | |||||||||||
| * substr(pos) returns a zstring_view because the tail remains nul-terminated. substr(pos, count) returns a | ||||||||||||
| std::string_view because an arbitrary slice may not be nul-terminated. | ||||||||||||
| * contains() is available before C++23 through a compatibility implementation. | ||||||||||||
| * nonnull_zstring_view uses a traits policy so its constructors produce non-null data(), including after default | ||||||||||||
| construction. | ||||||||||||
| */ | ||||||||||||
| template <class TChar> | ||||||||||||
| class basic_zstring_view : public std::basic_string_view<TChar> | ||||||||||||
| template <class TChar, class Traits> | ||||||||||||
| class basic_zstring_view : public std::basic_string_view<TChar, typename details::zstring_view_traits<TChar, Traits>::char_traits> | ||||||||||||
| { | ||||||||||||
| using size_type = typename std::basic_string_view<TChar>::size_type; | ||||||||||||
| using ZStringViewTraits = details::zstring_view_traits<TChar, Traits>; | ||||||||||||
| using BaseType = std::basic_string_view<TChar, typename ZStringViewTraits::char_traits>; | ||||||||||||
| using size_type = typename BaseType::size_type; | ||||||||||||
|
|
||||||||||||
| template <class, class> | ||||||||||||
| friend class basic_zstring_view; | ||||||||||||
|
|
||||||||||||
| template <typename T> | ||||||||||||
| struct has_c_str | ||||||||||||
|
|
@@ -196,47 +255,94 @@ class basic_zstring_view : public std::basic_string_view<TChar> | |||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| public: | ||||||||||||
| constexpr basic_zstring_view() noexcept = default; | ||||||||||||
| constexpr basic_zstring_view() noexcept : BaseType(default_view()) | ||||||||||||
| { | ||||||||||||
| } | ||||||||||||
| constexpr basic_zstring_view(const basic_zstring_view&) noexcept = default; | ||||||||||||
| constexpr basic_zstring_view& operator=(const basic_zstring_view&) noexcept = default; | ||||||||||||
|
|
||||||||||||
| constexpr basic_zstring_view(const TChar* pStringData, size_type stringLength) noexcept : | ||||||||||||
| std::basic_string_view<TChar>(pStringData, stringLength) | ||||||||||||
| BaseType(require_non_null(pStringData), stringLength) | ||||||||||||
| { | ||||||||||||
| if (pStringData[stringLength] != 0) | ||||||||||||
| if constexpr (ZStringViewTraits::empty_strings_are_non_null) | ||||||||||||
| { | ||||||||||||
| // The test harness records fail-fast and returns, so do not dereference a rejected null pointer afterward. | ||||||||||||
| if ((pStringData != nullptr) && (pStringData[stringLength] != 0)) | ||||||||||||
| { | ||||||||||||
| WI_STL_FAIL_FAST_IF(true); | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| else if (pStringData[stringLength] != 0) | ||||||||||||
| { | ||||||||||||
| WI_STL_FAIL_FAST_IF(true); | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+267
to
278
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you take my suggestion from down below, this all simplifies to a single call to |
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| template <size_t stringArrayLength> | ||||||||||||
| constexpr basic_zstring_view(const TChar (&stringArray)[stringArrayLength]) noexcept : | ||||||||||||
| std::basic_string_view<TChar>(&stringArray[0], length_n(&stringArray[0], stringArrayLength)) | ||||||||||||
| BaseType(&stringArray[0], length_n(&stringArray[0], stringArrayLength)) | ||||||||||||
| { | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| template <typename T = Traits, std::enable_if_t<details::zstring_view_traits<TChar, T>::empty_strings_are_non_null, int> = 0> | ||||||||||||
| basic_zstring_view(std::nullptr_t) = delete; | ||||||||||||
|
Comment on lines
+287
to
+288
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The SFINAE is probably unnecessary here if I'm understanding things correctly. For |
||||||||||||
|
|
||||||||||||
| // Construct from nul-terminated char ptr. To prevent this from overshadowing array construction, | ||||||||||||
| // we disable this constructor if the value is an array (including string literal). | ||||||||||||
| template <typename TPtr, std::enable_if_t<std::is_convertible<TPtr, const TChar*>::value && !std::is_array<TPtr>::value>* = nullptr> | ||||||||||||
| constexpr basic_zstring_view(TPtr&& pStr) noexcept : std::basic_string_view<TChar>(std::forward<TPtr>(pStr)) | ||||||||||||
| constexpr basic_zstring_view(TPtr&& pStr) noexcept : BaseType(require_non_null(std::forward<TPtr>(pStr))) | ||||||||||||
| { | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| constexpr basic_zstring_view(const std::basic_string<TChar>& str) noexcept : BaseType(&str[0], str.size()) | ||||||||||||
| { | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| constexpr basic_zstring_view(const std::basic_string<TChar>& str) noexcept : | ||||||||||||
| std::basic_string_view<TChar>(&str[0], str.size()) | ||||||||||||
| template < | ||||||||||||
| typename TSrc, | ||||||||||||
| std::enable_if_t< | ||||||||||||
| has_c_str<TSrc>::value && has_size<TSrc>::value && std::is_same_v<typename TSrc::value_type, TChar> && | ||||||||||||
| !details::is_basic_zstring_view<std::decay_t<TSrc>>::value>* = nullptr> | ||||||||||||
| constexpr basic_zstring_view(TSrc const& src) noexcept : BaseType(require_non_null(src.c_str()), src.size()) | ||||||||||||
| { | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| template <typename TSrc, std::enable_if_t<has_c_str<TSrc>::value && has_size<TSrc>::value && std::is_same_v<typename TSrc::value_type, TChar>>* = nullptr> | ||||||||||||
| constexpr basic_zstring_view(TSrc const& src) noexcept : std::basic_string_view<TChar>(src.c_str(), src.size()) | ||||||||||||
| template < | ||||||||||||
| typename TSrc, | ||||||||||||
| std::enable_if_t< | ||||||||||||
| has_c_str<TSrc>::value && !has_size<TSrc>::value && std::is_same_v<typename TSrc::value_type, TChar> && | ||||||||||||
| !details::is_basic_zstring_view<std::decay_t<TSrc>>::value>* = nullptr> | ||||||||||||
| constexpr basic_zstring_view(TSrc const& src) noexcept : BaseType(require_non_null(src.c_str())) | ||||||||||||
| { | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| template <typename TSrc, std::enable_if_t<has_c_str<TSrc>::value && !has_size<TSrc>::value && std::is_same_v<typename TSrc::value_type, TChar>>* = nullptr> | ||||||||||||
| constexpr basic_zstring_view(TSrc const& src) noexcept : std::basic_string_view<TChar>(src.c_str()) | ||||||||||||
| template < | ||||||||||||
| typename OtherTraits, | ||||||||||||
| std::enable_if_t< | ||||||||||||
| !std::is_same_v<Traits, OtherTraits> && std::is_same_v<BaseType, typename basic_zstring_view<TChar, OtherTraits>::BaseType> && | ||||||||||||
| (!ZStringViewTraits::empty_strings_are_non_null || details::zstring_view_traits<TChar, OtherTraits>::empty_strings_are_non_null), | ||||||||||||
| int> = 0> | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why so many different types used with |
||||||||||||
| constexpr basic_zstring_view(const basic_zstring_view<TChar, OtherTraits>& other) noexcept : | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You've added these "converting constructors" but did not do the same for the assignment operator. Consider if that should also be covered. |
||||||||||||
| BaseType(other.data(), other.size()) | ||||||||||||
| { | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| template < | ||||||||||||
| typename OtherTraits, | ||||||||||||
| std::enable_if_t< | ||||||||||||
| !std::is_same_v<Traits, OtherTraits> && std::is_same_v<BaseType, typename basic_zstring_view<TChar, OtherTraits>::BaseType> && | ||||||||||||
| ZStringViewTraits::empty_strings_are_non_null && !details::zstring_view_traits<TChar, OtherTraits>::empty_strings_are_non_null, | ||||||||||||
| long> = 0> | ||||||||||||
| explicit constexpr basic_zstring_view(const basic_zstring_view<TChar, OtherTraits>& other) noexcept : | ||||||||||||
| BaseType(require_non_null(other.data()), other.size()) | ||||||||||||
| { | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| template < | ||||||||||||
| typename OtherTraits, | ||||||||||||
| std::enable_if_t<!std::is_same_v<Traits, OtherTraits> && !std::is_same_v<BaseType, typename basic_zstring_view<TChar, OtherTraits>::BaseType>, short> = 0> | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The base types being different would imply that the traits types are different. |
||||||||||||
| basic_zstring_view(const basic_zstring_view<TChar, OtherTraits>&) = delete; | ||||||||||||
|
|
||||||||||||
| // basic_string_view [] precondition won't let us read view[view.size()]; so we define our own. | ||||||||||||
| WI_NODISCARD constexpr const TChar& operator[](size_type idx) const noexcept | ||||||||||||
| { | ||||||||||||
|
|
@@ -246,14 +352,18 @@ class basic_zstring_view : public std::basic_string_view<TChar> | |||||||||||
|
|
||||||||||||
| WI_NODISCARD constexpr const TChar* c_str() const noexcept | ||||||||||||
| { | ||||||||||||
| if constexpr (ZStringViewTraits::empty_strings_are_non_null) | ||||||||||||
| { | ||||||||||||
| WI_ASSERT(this->data() != nullptr); | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+355
to
+358
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Unless this triggers a bunch of "conditional expression is constant" warnings, I'd say to optimize for lines of code for debug-only statements. |
||||||||||||
| WI_ASSERT(this->data() == nullptr || this->data()[this->size()] == 0); | ||||||||||||
| return this->data(); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // contains() backport for builds below C++23. Compiles out once the STL provides | ||||||||||||
| // basic_string_view::contains natively. | ||||||||||||
| #if !defined(__cpp_lib_string_contains) || __cpp_lib_string_contains < 202011L | ||||||||||||
| WI_NODISCARD constexpr bool contains(std::basic_string_view<TChar> view) const noexcept | ||||||||||||
| WI_NODISCARD constexpr bool contains(BaseType view) const noexcept | ||||||||||||
| { | ||||||||||||
| return this->find(view) != this->npos; | ||||||||||||
| } | ||||||||||||
|
|
@@ -271,20 +381,45 @@ class basic_zstring_view : public std::basic_string_view<TChar> | |||||||||||
|
|
||||||||||||
| WI_NODISCARD constexpr basic_zstring_view substr(size_type pos = 0) const | ||||||||||||
| { | ||||||||||||
| const auto tail = std::basic_string_view<TChar>(*this).substr(pos); | ||||||||||||
| const auto tail = BaseType(*this).substr(pos); | ||||||||||||
| return tail.data() == nullptr ? basic_zstring_view{} : basic_zstring_view{tail.data(), tail.size()}; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| WI_NODISCARD constexpr std::basic_string_view<TChar> substr(size_type pos, size_type count) const | ||||||||||||
| WI_NODISCARD constexpr BaseType substr(size_type pos, size_type count) const | ||||||||||||
| { | ||||||||||||
| return std::basic_string_view<TChar>(*this).substr(pos, count); | ||||||||||||
| return BaseType(*this).substr(pos, count); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| private: | ||||||||||||
| static constexpr BaseType default_view() noexcept | ||||||||||||
| { | ||||||||||||
| if constexpr (ZStringViewTraits::empty_strings_are_non_null) | ||||||||||||
| { | ||||||||||||
| return BaseType(&details::zstring_view_empty_storage<TChar>[0], 0); | ||||||||||||
| } | ||||||||||||
| else | ||||||||||||
| { | ||||||||||||
| return BaseType{}; | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| static constexpr const TChar* require_non_null(const TChar* value) noexcept | ||||||||||||
| { | ||||||||||||
| if constexpr (ZStringViewTraits::empty_strings_are_non_null) | ||||||||||||
| { | ||||||||||||
| value = WI_STL_FAIL_FAST_IF_NULL(value); | ||||||||||||
| if (value == nullptr) | ||||||||||||
| { | ||||||||||||
| return &details::zstring_view_empty_storage<TChar>[0]; | ||||||||||||
| } | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fairly certain I know what this is trying to do and why, however someone less familiar with how the tests are structured and work could easily look at this and think there's a mistake or something of that nature, so I'd like to try and reduce the complexity here, which I believe should be possible. The best suggestion I have at the moment is to change this to something more like: template <bool CheckTerminator>
void check()
{
[[maybe_unused]] auto ptr = this->data();
[[maybe_unused]] auto len = this->size();
if constexpr(CheckTerminator && ZStringViewTraits::empty_strings_are_non_null)
{
WI_STL_FAIL_FAST_IF(!ptr || (ptr[len] != 0));
}
else if constexpr (ZStringViewTraits::empty_strings_are_non_null)
{
WI_STL_FAIL_FAST_IF(!ptr);
}
else if constexpr (CheckTerminator)
{
WI_STL_FAIL_FAST_IF(ptr[len] != 0);
}
}Effectively, this combines the two checks - null and null terminated - into a single fail-fast check. That is, you wouldn't have the issue where a "fail-fast" would get issued, recorded in the test, and then continue execution only to crash on a null pointer read. You could then modify the constructors as follows (
|
||||||||||||
| } | ||||||||||||
| return value; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Bounds-checked version of char_traits::length, like strnlen. Requires that the input contains a null terminator. | ||||||||||||
| static constexpr size_type length_n(_In_reads_opt_(buf_size) const TChar* str, size_type buf_size) noexcept | ||||||||||||
| { | ||||||||||||
| const std::basic_string_view<TChar> view(str, buf_size); | ||||||||||||
| const BaseType view(str, buf_size); | ||||||||||||
| auto pos = view.find_first_of(TChar()); | ||||||||||||
| if (pos == view.npos) | ||||||||||||
| { | ||||||||||||
|
|
@@ -294,17 +429,21 @@ class basic_zstring_view : public std::basic_string_view<TChar> | |||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // The following basic_string_view methods must not be allowed because they break the nul-termination. | ||||||||||||
| using std::basic_string_view<TChar>::swap; | ||||||||||||
| using std::basic_string_view<TChar>::remove_suffix; | ||||||||||||
| using BaseType::remove_suffix; | ||||||||||||
| using BaseType::swap; | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| using zstring_view = basic_zstring_view<char>; | ||||||||||||
| using zwstring_view = basic_zstring_view<wchar_t>; | ||||||||||||
|
|
||||||||||||
| // Variants that reject null construction and default to a non-null empty string. | ||||||||||||
| using nonnull_zstring_view = basic_zstring_view<char, nonnull_zstring_view_traits<char>>; | ||||||||||||
| using nonnull_zwstring_view = basic_zstring_view<wchar_t, nonnull_zstring_view_traits<wchar_t>>; | ||||||||||||
|
|
||||||||||||
| // str_raw_ptr is an overloaded function that retrieves a const pointer to the first character in a string's buffer. | ||||||||||||
| // This is the overload for std::wstring. Other overloads available in resource.h. | ||||||||||||
| template <typename TChar> | ||||||||||||
| inline auto str_raw_ptr(basic_zstring_view<TChar> str) | ||||||||||||
| template <typename TChar, typename Traits> | ||||||||||||
| inline auto str_raw_ptr(basic_zstring_view<TChar, Traits> str) | ||||||||||||
| { | ||||||||||||
| return str.c_str(); | ||||||||||||
| } | ||||||||||||
|
|
@@ -423,8 +562,9 @@ overloaded(T...) -> overloaded<T...>; | |||||||||||
| #ifndef WIL_SUPPRESS_STD_FORMAT_USE | ||||||||||||
| #if (__WI_LIBCPP_STD_VER >= 20) && WI_HAS_INCLUDE(<format>, 1) // Assume present if C++20 | ||||||||||||
| #include <format> | ||||||||||||
| template <typename TChar> | ||||||||||||
| struct std::formatter<wil::basic_zstring_view<TChar>, TChar> : std::formatter<std::basic_string_view<TChar>, TChar> | ||||||||||||
| template <typename TChar, typename Traits> | ||||||||||||
| struct std::formatter<wil::basic_zstring_view<TChar, Traits>, TChar> | ||||||||||||
| : std::formatter<std::basic_string_view<TChar, typename wil::details::zstring_view_traits<TChar, Traits>::char_traits>, TChar> | ||||||||||||
| { | ||||||||||||
| }; | ||||||||||||
| #endif | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the other definition was to work around a conflict with
FAIL_FAST_IF. AFAIK such a conflict doesn't exist forFAIL_FAST_IF_NULL. That said, if you take my suggestion, this define isn't needed anyway