AK: Remove public null state from StringView

Now that there are no callers of is_null left, Make default constructed
StringViews represent the empty string and disallow null pointers in the
public constructors, matching ByteString and String.

Keep a private null sentinel only for Optional<StringView>.
This commit is contained in:
Shannon Booth
2026-03-31 00:51:28 +02:00
committed by Sam Atkins
parent 7ab9454f51
commit e47cdc6b63
Notes: github-actions[bot] 2026-03-31 12:50:02 +00:00
7 changed files with 46 additions and 34 deletions

View File

@@ -23,9 +23,6 @@ bool ByteString::operator==(ByteString const& other) const
bool ByteString::operator==(StringView other) const
{
if (other.is_null())
return is_empty();
return view() == other;
}

View File

@@ -29,9 +29,6 @@ bool matches(StringView str, StringView mask, CaseSensitivity case_sensitivity,
match_spans->append({ start, length });
};
if (str.is_null() || mask.is_null())
return str.is_null() && mask.is_null();
if (mask == "*"sv) {
record_span(0, str.length());
return true;
@@ -148,7 +145,7 @@ bool starts_with(StringView str, StringView start, CaseSensitivity case_sensitiv
bool contains(StringView str, StringView needle, CaseSensitivity case_sensitivity)
{
if (str.is_null() || needle.is_null() || str.is_empty() || needle.length() > str.length())
if (str.is_empty() || needle.length() > str.length())
return false;
if (needle.is_empty())
return true;

View File

@@ -41,6 +41,7 @@ StringView::StringView(ByteBuffer const& buffer)
: m_characters((char const*)buffer.data())
, m_length(buffer.size())
{
VERIFY(buffer.data() != nullptr);
}
Vector<StringView> StringView::split_view(char const separator, SplitBehavior split_behavior) const

View File

@@ -7,6 +7,7 @@
#pragma once
#include <AK/Assertions.h>
#include <AK/Badge.h>
#include <AK/Checked.h>
#include <AK/Concepts.h>
#include <AK/Forward.h>
@@ -28,6 +29,7 @@ public:
, m_length(length)
{
if !consteval {
VERIFY(characters != nullptr);
VERIFY(!Checked<uintptr_t>::addition_would_overflow(reinterpret_cast<uintptr_t>(characters), length));
}
}
@@ -36,6 +38,7 @@ public:
: m_characters(reinterpret_cast<char const*>(characters))
, m_length(length)
{
VERIFY(characters != nullptr);
VERIFY(!Checked<uintptr_t>::addition_would_overflow(reinterpret_cast<uintptr_t>(characters), length));
}
@@ -43,6 +46,7 @@ public:
: m_characters(reinterpret_cast<char const*>(bytes.data()))
, m_length(bytes.size())
{
VERIFY(bytes.data() != nullptr);
}
StringView(LIFETIME_BOUND ByteBuffer const&);
@@ -58,7 +62,6 @@ public:
template<OneOf<String, FlyString, ByteString, ByteBuffer> StringType>
StringView& operator=(StringType&&) = delete;
[[nodiscard]] constexpr bool is_null() const { return m_characters == nullptr; }
[[nodiscard]] constexpr bool is_empty() const { return m_length == 0; }
[[nodiscard]] constexpr char const* characters_without_null_termination() const { return m_characters; }
@@ -253,8 +256,6 @@ public:
constexpr bool operator==(char const* cstring) const
{
if (is_null())
return cstring == nullptr;
if (!cstring)
return false;
// NOTE: `m_characters` is not guaranteed to be null-terminated, but `cstring` is.
@@ -280,12 +281,6 @@ public:
if (m_length == 0 && other.m_length == 0)
return 0;
if (m_characters == nullptr)
return other.m_characters ? -1 : 0;
if (other.m_characters == nullptr)
return 1;
size_t rlen = min(m_length, other.m_length);
int c = __builtin_memcmp(m_characters, other.m_characters, rlen);
if (c == 0) {
@@ -358,10 +353,30 @@ public:
private:
friend class ByteString;
char const* m_characters { nullptr };
friend struct SentinelOptionalTraits<StringView>;
explicit constexpr StringView(Badge<SentinelOptionalTraits<StringView>>, nullptr_t)
: m_characters(nullptr)
, m_length(0)
{
}
char const* m_characters { "" };
size_t m_length { 0 };
};
template<>
struct SentinelOptionalTraits<StringView> {
static constexpr StringView sentinel_value() { return StringView(Badge<SentinelOptionalTraits<StringView>> {}, nullptr); }
static constexpr bool is_sentinel(StringView const& value) { return value.m_characters == nullptr; }
};
template<>
class Optional<StringView> : public SentinelOptional<StringView> {
public:
using SentinelOptional::SentinelOptional;
};
template<>
struct Traits<StringView> : public DefaultTraits<StringView> {
using PeekType = StringView;

View File

@@ -110,7 +110,6 @@ public:
Utf8View unicode_substring_view(size_t code_point_offset) const { return unicode_substring_view(code_point_offset, length() - code_point_offset); }
bool is_empty() const { return m_string.is_empty(); }
bool is_null() const { return m_string.is_null(); }
bool starts_with(Utf8View const&) const;
bool contains(u32) const;
bool contains_any_of(ReadonlySpan<u32>) const;

View File

@@ -29,19 +29,6 @@ TEST_CASE(hash_compatible)
static_assert(AK::Concepts::HashCompatible<ByteBuffer, StringView>);
}
TEST_CASE(matches_null)
{
EXPECT(AK::StringUtils::matches(StringView(), StringView()));
EXPECT(!AK::StringUtils::matches(StringView(), ""sv));
EXPECT(!AK::StringUtils::matches(StringView(), "*"sv));
EXPECT(!AK::StringUtils::matches(StringView(), "?"sv));
EXPECT(!AK::StringUtils::matches(StringView(), "a"sv));
EXPECT(!AK::StringUtils::matches(""sv, StringView()));
EXPECT(!AK::StringUtils::matches("a"sv, StringView()));
}
TEST_CASE(matches_empty)
{
EXPECT(AK::StringUtils::matches(""sv, ""sv));

View File

@@ -11,9 +11,8 @@
TEST_CASE(construct_empty)
{
EXPECT(StringView().is_null());
EXPECT(StringView().is_empty());
EXPECT(!StringView().characters_without_null_termination());
EXPECT_EQ(StringView().characters_without_null_termination(), "");
EXPECT_EQ(StringView().length(), 0u);
}
@@ -21,12 +20,29 @@ TEST_CASE(view_literal)
{
char const* truth = "cats rule dogs drool";
StringView view { truth, strlen(truth) };
EXPECT_EQ(view.is_null(), false);
EXPECT_EQ(view.characters_without_null_termination(), truth);
EXPECT_EQ(view, view);
EXPECT_EQ(view, truth);
}
TEST_CASE(optional_string_view)
{
Optional<StringView> missing;
EXPECT(!missing.has_value());
Optional<StringView> empty = ""sv;
EXPECT(empty.has_value());
EXPECT(empty->is_empty());
EXPECT_EQ(empty.value(), ""sv);
Optional<StringView> value = "foo"sv;
EXPECT(value.has_value());
EXPECT_EQ(value.value(), "foo"sv);
value.clear();
EXPECT(!value.has_value());
}
TEST_CASE(compare_views)
{
ByteString foo1 = "foo";