zstring: Add class

This is the most efficient wrapper for nul-terminated strings as it does
not care about determining string length. Conversions to / from
ztring_view are provided to make it trivial to upgrade.

Change-Id: Ic83626c53b95b851c3c4a13b1eded8f1e40539f0
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/include/meson.build b/include/meson.build
index 91ea750..67ad4f7 100644
--- a/include/meson.build
+++ b/include/meson.build
@@ -6,6 +6,7 @@
   'stdplus/flags.hpp',
   'stdplus/raw.hpp',
   'stdplus/signal.hpp',
+  'stdplus/zstring.hpp',
   'stdplus/zstring_view.hpp',
   subdir: 'stdplus')
 
diff --git a/include/stdplus/zstring.hpp b/include/stdplus/zstring.hpp
new file mode 100644
index 0000000..28a3494
--- /dev/null
+++ b/include/stdplus/zstring.hpp
@@ -0,0 +1,241 @@
+#pragma once
+#include <cstddef>
+#include <limits>
+#include <string>
+#include <type_traits>
+#ifndef NDEBUG
+#include <stdexcept>
+#endif
+
+namespace stdplus
+{
+namespace detail
+{
+
+template <typename CharT, std::size_t N>
+constexpr bool zstring_validate(const CharT (&str)[N]) noexcept
+{
+    for (size_t i = 0; i < N - 1; ++i)
+    {
+        if (str[i] == '\0')
+        {
+            return false;
+        }
+    }
+    return str[N - 1] == '\0';
+}
+
+template <typename CharT, typename Traits>
+inline constexpr bool
+    zstring_validate(const std::basic_string<CharT, Traits>& str) noexcept
+{
+    return str.find('\0') == str.npos;
+}
+
+} // namespace detail
+
+/**
+ * @brief Provides a class for expressing an unbounded length string that has
+ * a nul terminating character. This is different from zstring_view in that it
+ * never computes the length, which is slightly cheaper if the string will just
+ * be downcast to a `const char *`.
+ */
+template <typename CharT,
+          typename Traits = std::char_traits<std::remove_const_t<CharT>>>
+class basic_zstring
+{
+  private:
+    using decay_t = std::remove_const_t<CharT>;
+
+  public:
+    using traits_type = Traits;
+    using value_type = CharT;
+    using pointer = value_type*;
+    using const_pointer = const value_type*;
+    using reference = value_type&;
+    using const_reference = const value_type&;
+    using size_type = std::size_t;
+
+    template <typename T, size_type N,
+              std::enable_if_t<std::is_same_v<decay_t, std::remove_cvref_t<T>>,
+                               bool> = true>
+    constexpr basic_zstring(T (&str)[N])
+#ifdef NDEBUG
+        noexcept
+#endif
+        :
+        data_(str)
+    {
+#ifndef NDEBUG
+        if (!detail::zstring_validate(str))
+        {
+            throw std::invalid_argument("stdplus::zstring");
+        }
+#endif
+    }
+    template <typename T, std::enable_if_t<std::is_pointer_v<T>, bool> = true>
+    inline constexpr basic_zstring(T str) noexcept : data_(str)
+    {
+    }
+    template <typename T,
+              std::enable_if_t<
+                  std::is_same_v<std::basic_string<decay_t, Traits,
+                                                   typename T::allocator_type>,
+                                 std::remove_cvref_t<T>>,
+                  bool> = true>
+    constexpr basic_zstring(T& str)
+#ifdef NDEBUG
+        noexcept
+#endif
+        :
+        data_(str.data())
+    {
+#ifndef NDEBUG
+        if (!detail::zstring_validate(str))
+        {
+            throw std::invalid_argument("stdplus::zstring");
+        }
+#endif
+    }
+
+    inline constexpr
+        operator basic_zstring<const CharT, Traits>() const noexcept
+    {
+        return basic_zstring<const CharT, Traits>(data_);
+    }
+
+    inline constexpr reference operator[](size_type pos) const noexcept
+    {
+        return data_[pos];
+    }
+    inline constexpr reference front() const noexcept
+    {
+        return data_[0];
+    }
+    inline constexpr pointer data() const noexcept
+    {
+        return data_;
+    }
+    inline constexpr const_pointer c_str() const noexcept
+    {
+        return data_;
+    }
+    [[nodiscard]] constexpr bool empty() const noexcept
+    {
+        return data_ == nullptr || data_[0] == '\0';
+    }
+
+    inline constexpr basic_zstring suffix(size_type size) const noexcept
+    {
+        return data_ + size;
+    }
+    inline constexpr basic_zstring<const CharT, Traits>
+        csuffix(size_type size) const noexcept
+    {
+        return data_ + size;
+    }
+
+    constexpr int compare(const_pointer other,
+                          size_type other_size) const noexcept
+    {
+        for (size_t i = 0; i < other_size; ++i)
+        {
+            if (data_[i] == '\0')
+            {
+                return -1;
+            }
+            if (!Traits::eq(other[i], data_[i]))
+            {
+                return Traits::lt(other[i], data_[i]) ? 1 : -1;
+            }
+        }
+        if (data_[other_size] == '\0')
+        {
+            return 0;
+        }
+        return 1;
+    }
+    constexpr int compare(const_pointer other) const noexcept
+    {
+        auto data = data_;
+        while (true)
+        {
+            if (data[0] == '\0')
+            {
+                if (other[0] == '\0')
+                {
+                    return 0;
+                }
+                return -1;
+            }
+            if (other[0] == '\0')
+            {
+                return 1;
+            }
+            if (!Traits::eq(other[0], data[0]))
+            {
+                return Traits::lt(other[0], data[0]) ? 1 : -1;
+            }
+            data++;
+            other++;
+        }
+    }
+
+    template <typename CharT1>
+    inline constexpr Traits::comparison_category
+        operator<=>(basic_zstring<CharT1, Traits> rhs) const noexcept
+    {
+        return compare(rhs.data()) <=> 0;
+    }
+    template <typename CharTO, size_type N>
+    inline constexpr Traits::comparison_category
+        operator<=>(const CharTO (&rhs)[N]) const noexcept
+    {
+        return compare(rhs, N - 1) <=> 0;
+    }
+    template <typename T, std::enable_if_t<std::is_pointer_v<T>, bool> = true>
+    inline constexpr Traits::comparison_category
+        operator<=>(T rhs) const noexcept
+    {
+        return compare(rhs) <=> 0;
+    }
+    template <typename Allocator>
+    inline constexpr Traits::comparison_category operator<=>(
+        const std::basic_string<decay_t, Traits, Allocator>& rhs) const noexcept
+    {
+        return compare(rhs.data(), rhs.size()) <=> 0;
+    }
+    inline constexpr Traits::comparison_category
+        operator<=>(std::basic_string_view<decay_t, Traits> rhs) const noexcept
+    {
+        return compare(rhs.data(), rhs.size()) <=> 0;
+    }
+
+    inline constexpr bool operator==(const auto& rhs) const noexcept
+    {
+        return (*this <=> rhs) == 0;
+    }
+
+  private:
+    pointer data_;
+};
+
+template <typename CharT, typename Traits>
+std::basic_ostream<std::remove_const_t<CharT>, Traits>&
+    operator<<(std::basic_ostream<std::remove_const_t<CharT>, Traits>& os,
+               basic_zstring<CharT, Traits> v)
+{
+    return os << v.c_str();
+}
+
+#define zstring_all(char_t, pfx)                                               \
+    using pfx##zstring = basic_zstring<char_t>;                                \
+    using const_##pfx##zstring = basic_zstring<const char_t>;
+zstring_all(char, );
+zstring_all(char8_t, u8);
+zstring_all(char16_t, u16);
+zstring_all(char32_t, u32);
+zstring_all(wchar_t, w);
+#undef zstring_all
+
+} // namespace stdplus
diff --git a/include/stdplus/zstring_view.hpp b/include/stdplus/zstring_view.hpp
index babb1c2..c97364c 100644
--- a/include/stdplus/zstring_view.hpp
+++ b/include/stdplus/zstring_view.hpp
@@ -1,5 +1,6 @@
 #pragma once
 #include <stdexcept>
+#include <stdplus/zstring.hpp>
 #include <string>
 #include <string_view>
 #include <type_traits>
@@ -27,19 +28,6 @@
 }
 
 template <typename CharT, std::size_t N>
-constexpr bool zstring_validate(const CharT (&str)[N]) noexcept
-{
-    for (size_t i = 0; i < N - 1; ++i)
-    {
-        if (str[i] == '\0')
-        {
-            return false;
-        }
-    }
-    return str[N - 1] == '\0';
-}
-
-template <typename CharT, std::size_t N>
 struct compile_zstring_view
 {
     CharT data[N] = {};
@@ -67,6 +55,7 @@
 {
   private:
     using string_view_base = std::basic_string_view<CharT, Traits>;
+    using zstring_base = basic_zstring<const CharT, Traits>;
 
   public:
     using traits_type = string_view_base::traits_type;
@@ -120,18 +109,31 @@
         sv(str.data())
     {
 #ifndef NDEBUG
-        if (str.find('\0') != npos)
+        if (!detail::zstring_validate(str))
         {
             throw std::invalid_argument("stdplus::zstring_view");
         }
 #endif
     }
+    template <
+        typename T,
+        std::enable_if_t<std::is_same_v<value_type, std::remove_const_t<T>>,
+                         bool> = true>
+    inline constexpr basic_zstring_view(basic_zstring<T, Traits> str) noexcept :
+        sv(str.data())
+    {
+    }
 
     inline constexpr operator string_view_base() const noexcept
     {
         return sv;
     }
 
+    inline constexpr operator zstring_base() const noexcept
+    {
+        return zstring_base(data());
+    }
+
     inline constexpr const_iterator begin() const noexcept
     {
         return sv.begin();
diff --git a/src/meson.build b/src/meson.build
index a59cffd..204110f 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -44,6 +44,7 @@
 stdplus_srcs = [
   'exception.cpp',
   'signal.cpp',
+  'zstring.cpp',
   'zstring_view.cpp',
 ]
 
diff --git a/src/zstring.cpp b/src/zstring.cpp
new file mode 100644
index 0000000..660b81c
--- /dev/null
+++ b/src/zstring.cpp
@@ -0,0 +1,16 @@
+#include <stdplus/zstring.hpp>
+
+namespace stdplus
+{
+
+#define zstring_instance(char_t)                                               \
+    template class basic_zstring<char_t>;                                      \
+    template class basic_zstring<const char_t>
+zstring_instance(char);
+zstring_instance(char8_t);
+zstring_instance(char16_t);
+zstring_instance(char32_t);
+zstring_instance(wchar_t);
+#undef zstring_instance
+
+} // namespace stdplus
diff --git a/test/meson.build b/test/meson.build
index ade258e..2a6645f 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -7,6 +7,7 @@
   'signal': [stdplus_dep, gtest_main_dep],
   'util/cexec': [stdplus_dep, gtest_main_dep],
   'util/string': [stdplus_dep, gtest_main_dep],
+  'zstring': [stdplus_dep, gtest_main_dep],
   'zstring_view': [stdplus_dep, gtest_main_dep],
 }
 
diff --git a/test/zstring.cpp b/test/zstring.cpp
new file mode 100644
index 0000000..07ea1f1
--- /dev/null
+++ b/test/zstring.cpp
@@ -0,0 +1,103 @@
+#include <gtest/gtest.h>
+#include <iostream>
+#include <stdplus/zstring.hpp>
+#include <string>
+#include <string_view>
+
+using std::literals::string_literals::operator""s;
+using std::literals::string_view_literals::operator""sv;
+
+namespace stdplus
+{
+
+TEST(Zstring, Construct)
+{
+    auto str = "a\0"s;
+    const auto cstr = str;
+#ifdef NDEBUG
+    EXPECT_EQ("a", zstring(str));
+    EXPECT_EQ("a", const_zstring(str));
+    EXPECT_EQ("a", const_zstring(cstr));
+#else
+    EXPECT_THROW((zstring(str)), std::invalid_argument);
+    EXPECT_THROW((const_zstring(str)), std::invalid_argument);
+    EXPECT_THROW((const_zstring(cstr)), std::invalid_argument);
+#endif
+#ifdef NDEBUG
+    EXPECT_EQ("b", const_zstring("b\0"));
+#else
+    EXPECT_THROW(const_zstring("b\0"), std::invalid_argument);
+#endif
+    char as[] = "c";
+    EXPECT_EQ("c", zstring(as));
+    EXPECT_EQ("c", const_zstring(as));
+
+    std::cerr << const_zstring(as);
+}
+
+TEST(Zstring, NoTypeCoercion)
+{
+    char empty[] = "";
+    auto zs = zstring(empty);
+    auto czs = const_zstring("");
+
+    EXPECT_NE(zs, "\0");
+    EXPECT_NE("\0", zs);
+    EXPECT_NE(zs, "\0"sv);
+    EXPECT_NE("\0"sv, zs);
+    EXPECT_LT(zs, "\0"sv);
+    EXPECT_GT("\0"sv, zs);
+    EXPECT_NE(czs, "\0");
+    EXPECT_NE("\0", czs);
+    EXPECT_NE(czs, "\0"sv);
+    EXPECT_NE("\0"sv, czs);
+    EXPECT_LT(czs, "\0"sv);
+    EXPECT_GT("\0"sv, czs);
+
+    auto str = "\0"s;
+    EXPECT_NE(zs, str);
+    EXPECT_NE(str, zs);
+    EXPECT_LT(zs, str);
+    EXPECT_GT(str, zs);
+    EXPECT_NE(czs, str);
+    EXPECT_NE(str, czs);
+    EXPECT_LT(czs, str);
+    EXPECT_GT(str, czs);
+}
+
+TEST(Zstring, Comparison)
+{
+    char mut[] = "ac";
+    auto zs = zstring(mut);
+    auto czs = const_zstring("ac");
+
+#define test(cmp, rcmp, str)                                                   \
+    EXPECT_##cmp(zs, const_zstring(str));                                      \
+    EXPECT_##rcmp(const_zstring(str), zs);                                     \
+    EXPECT_##cmp(czs, const_zstring(str));                                     \
+    EXPECT_##rcmp(const_zstring(str), czs);                                    \
+    EXPECT_##cmp(zs, str);                                                     \
+    EXPECT_##rcmp(str, zs);                                                    \
+    EXPECT_##cmp(czs, str);                                                    \
+    EXPECT_##rcmp(str, czs);                                                   \
+    EXPECT_##cmp(zs, str##sv);                                                 \
+    EXPECT_##rcmp(str##sv, zs);                                                \
+    EXPECT_##cmp(czs, str##sv);                                                \
+    EXPECT_##rcmp(str##sv, czs);                                               \
+    EXPECT_##cmp(zs, str##s);                                                  \
+    EXPECT_##rcmp(str##s, zs);                                                 \
+    EXPECT_##cmp(czs, str##s);                                                 \
+    EXPECT_##rcmp(str##s, czs);                                                \
+    EXPECT_##cmp(zs, reinterpret_cast<const char*>(str));                      \
+    EXPECT_##rcmp(reinterpret_cast<const char*>(str), zs);                     \
+    EXPECT_##cmp(czs, reinterpret_cast<const char*>(str));                     \
+    EXPECT_##rcmp(reinterpret_cast<const char*>(str), czs)
+    test(EQ, EQ, "ac");
+    test(GT, LT, "a");
+    test(LT, GT, "acb");
+    test(LT, GT, "ad");
+    test(GT, LT, "ab");
+#undef test
+}
+
+} // namespace stdplus