Ox has six different major string types. These types are divided into two categories: store types and view types.

String stores maintain a copy of the string data, whereas view types only maintain a reference to the data. Views should be used where you otherwise might use a const reference to a string store type.

Having all of these different string types may sound like an interoperability nightmare, but taking string view types extensively where applicable makes the imagined interoperability issues virtually non-existent.

String Store Types

String / BasicString

ox::String, or really ox::BasicString, is Ox’s version of std::string. Like std::string, String allocates to store the string data. Also like std::string, String allows for small string optimization for strings under 8 bytes.

Small String Size

Unlike std::string, the template that String is based on, BasicString, takes a parameter that allows adjusting to different size small string buffers. ox::String is an alias to ox::BasicString<8>.

// s can hold up to 100 bytes, plus one for a null terminator before allocating
ox::BasicString<100> s;

Explicit Constructor

Also unlike std::string, ox::String has an explicit C-string conversion constructor. This prevents accidental instantiations of String.

Consider the following:

void fStd(std::string const&);
void fOx(ox::String const&);

int main() {
    // implicit and silent instantiation of std::string, which includes an
    // allocation
    fStd("123456789");
    // Will fail to compile:
    fOx("123456789");
    // But explicit String instantiation will work:
    fOx(ox::String{"123456789"});
}

IString

IString, or “inline string”, is like BasicString, but it will cut off strings that exceed that limit.

ox::IString<5> s; // s can hold up to 5 characters, plus a null terminator
s = "12345";      // valid
s = "123456";     // will compile and run, but will get cut off at '5'

This is useful for certain string categories that have fixed lengths, like UUID strings or for numbers.

Ox makes use of IString in the following ways:

using UUIDStr = ox::IString<36>;

// and

template<Integer_c Integer>
[[nodiscard]]
constexpr auto intToStr(Integer v) noexcept {
	constexpr auto Cap = [] {
		auto out = 0;
		switch (sizeof(Integer)) {
			case 1:
				out = 3;
				break;
			case 2:
				out = 5;
				break;
			case 4:
				out = 10;
				break;
			case 8:
				out = 21;
				break;
		}
		return out + ox::is_signed_v<Integer>;
	}();
	ox::IString<Cap> out;
	std::ignore = out.resize(out.cap());
	ox::CharBuffWriter w{{out.data(), out.cap()}};
	std::ignore = writeItoa(v, w);
	std::ignore = out.resize(w.tellp());
	return out;
}

StringParam

StringParam is a weird type. Because String::String(const char*) is explicit, it becomes a pain for functions to take Strings.

struct Type {
    ox::String m_s;
    explicit Type(ox::String p): m_s(std::move(p)) {
    }
};

void f() {
    ox::String s{"asdf"};
    Type t1{"asdf"};             // invalid - will not compile
    Type t2{s};                  // invalid - will not compile
    Type t3{std::move(s)};       // valid
    Type t4{ox::String{"asdf"}}; // valid
}

StringParam has implicit conversion constructors, and will appropriately move from r-value Strings or create a String if not passed ownership of an existing String. Think of StringParam as a way to opt-in to implicit instantiation with String.

StringParam can access the string as a view through the view() function, and the String inside can be accessed by moving from the StringParam.

struct Type {
    ox::String m_s;
    explicit Type(ox::StringParam p): m_s(std::move(p)) {
    }
};

void f() {
    ox::String s{"asdf"};
    Type t1{"asdf"};       // valid
    Type t2{s};            // valid
    Type t3{std::move(s)}; // valid
}

String View Types

StringView

ox::StringView is Ox’s version of std::string_view. StringView contains a pointer to a string, along with its size.

This should be the normal type taken when a function needs a string that will exist until it returns.

CStringView

CStringView is like StringView, but it comes with the promise that the string ends with a null terminator. Accordingly, it has a c_str() function in addition to the data() function that StringView has.

CStringView should be used when wrapping a C API that only takes C strings.

StringLiteral

StringLiteral is a string view type, but it kind of straddles the line between view and store types. Creating a StringLiteral is a promise that you are passing a string literal into the constructor. This means you can treat it like a store, that can be safely used as a copy of the data. Functions that take StringLiterals are allowed to assume that the data will have no lifetime concerns and hold onto it without any need to make a copy. It has a consteval constructor to enforce the promise that it is a compile time string.

void f(ox::StringLiteral const&);

int main() {
    f("123456789");                     // valid
    f(ox::String{"123456789"}.c_str()); // invalid - will not compile
}

Other Variants

There are a few convenience aliases as well.

  • StringCR = String const&
  • StringViewCR = StringView const&
  • CStringViewCR = CStringView const&
  • CString = const char*

String views do not generally need const references, but it does make debugging easier, as we can skip the constructor call if a string view already exists.

These kind of aliases probably should not exist for most types, but strings are fundamental and ease of use is desirable.