Skip to content

IString / IStringView / IStringViewStash

Having a String Library for Everyone

In ICore, there are various string types such as std::string, QString, QByteArray, and const char* available to represent string data. So, why introduce additional complexity by creating a new IString?

QString is an excellent string library that stores strings in UTF-8 format. Therefore, if users want to obtain a byte stream, they must convert it using its toUtf8() function to get a QByteArray. QByteArray represents raw byte data. std::string stores a byte stream, but the encoding method is not specified. const char* typically holds string content in the static or global area, or possibly on the heap. To address these inconsistencies and conversion issues, we encapsulate these into a unified IString solution.

IString

Declaration

The declaration of IString is as follows:

struct IString
{
private:
    enum class Type {
        IStringView, // default, and empty
        QByteArray,
        StdString,
    };

public:
    IString() = default;
    ~IString();

    IString(const IString& other);
    IString(IString&& other) noexcept;
    IString& operator=(const IString& other);
    IString& operator=(IString&& other) noexcept;

    IString(const IString*);
    IString(const std::string*);
    IString(const QByteArray*);

    IString(const char*);
    IString(const QByteArray& byteArray);
    IString(QByteArray&& byteArray) noexcept;
    IString(const std::string& stdString);
    IString(std::string&& stdString) noexcept;
    IString(const QString&) noexcept;
    IString(IStringView&& stringView);
    IString(const IStringView& stringView);

    IString& operator=(const IString*);
    IString& operator=(const QByteArray*);
    IString& operator=(const std::string*);

    IString& operator=(const char*);
    IString& operator=(const QString&);
    IString& operator=(const QByteArray& byteArray);
    IString& operator=(QByteArray&& byteArray) noexcept;
    IString& operator=(const std::string& stdString);
    IString& operator=(std::string&& stdString) noexcept;
    IString& operator=(IStringView&& stringView);
    IString& operator=(const IStringView& view);
    IString& operator=(std::nullptr_t);

    bool operator ==(const IString&) const;
    bool operator !=(const IString&) const;
    bool operator <(const IString&) const;

public:
    std::size_t length() const;
    std::size_t size() const;
    bool isSolid() const;
    bool isEmpty() const;
    IString& solidify();
    operator bool() const;
    operator IStringView() const;

public:
    template<typename T>
    IResult<T> value() const;

public:
    IStringViewList split(char) const;
    IStringViewList split(const IString&) const;
    bool startWith(const IString&) const;
    bool equal(const IString&) const;
    bool equalIgnoreCase(const IString&) const;
    bool containIgnoreCase(const IString&) const;

public:
    QString toQString() const;
    std::string toStdString() const;
    QByteArray toQByteArray() const;

private:
    void clear();
    void copyFrom(const IString& other);
    void moveFrom(IString&& other) noexcept;

private:
    Type m_type{Type::IStringView};
    void* m_data{};

public:
    IStringView m_view{};
};

using IStringList = QList<IString>;

Q_DECLARE_METATYPE(IString)

In the code above, the constructors and assignment operators are the most frequently used. Users can pass any string type to construct an IString.

Construction

Users can construct an IString by passing the following types:

  • const char*, const std::string&, const QString&, const QByteArray&, const IString&, const IStringView&

When constructing IString with these types, the content is copied into IString, which stores a separate copy.

  • std::string&&, QByteArray&&, IStringView&&, IString&&

These types use move constructors to transfer the parameters into IString.

  • const IString*, const std::string*, const QByteArray*

These types are particularly interesting. For example, if a user has a std::string object and knows that the object will exist throughout the lifetime of the IString object, the user can pass the address of the std::string object to IString as std::string*/const std::string*. IString will store the object via std::string_view, thus avoiding unnecessary creation and copying.

Assignment operators follow a similar pattern.

Provided Function Interfaces

Querying Object Content

// Get the length of bytes
std::size_t length() const;
std::size_t size() const;

// Determine if the object is a solid object, i.e., whether it contains a copy of the string internally.
bool isSolid() const;

// Determine if the content is empty
bool isEmpty() const;

// For objects without a string copy, create a string copy.
IString& solidify();

// Determine if the object is empty
operator bool() const;

// Get the view of the object
operator IStringView() const;

Object Conversion

template<typename T>
IResult<T> value() const;

This is particularly interesting as it can convert the object into various types such as integer values, floating-point values, string values (std::string, QString, QByteArray, IStringView), or even types like QDate, QTime, and QDateTime. If the conversion fails, it returns a std::nullopt object, allowing the caller to check for failure.

String Operations

// Split the string
IStringViewList split(char) const;
IStringViewList split(const IString&) const;

// Check if the string starts with another string
bool startWith(const IString&) const;

// Check for equality
bool equal(const IString&) const;

// Check for equality, ignoring case
bool equalIgnoreCase(const IString&) const;

// Check for containment, ignoring case
bool containIgnoreCase(const IString&) const;

String Conversion

1
2
3
4
5
6
7
8
// Convert IString to QString type
QString toQString() const;

// Convert IString to std::string type
std::string toStdString() const;

// Convert IString to QByteArray type.
QByteArray toQByteArray() const;

IStringView

IStringView is a reimplementation of std::string_view. It contains two pieces of information: the starting address of the string and its length. The reason for re-implementing it is to provide additional functionality.

Declaration

The declaration of IStringView is as follows:

class IStringViewList;
class IStringView : public std::string_view
{
public:
    IStringView() = default;
    explicit IStringView(const std::string& data);
    explicit IStringView(const QByteArray& data);
    explicit constexpr IStringView(std::string_view data);
    explicit constexpr IStringView(const char* data, std::size_t length);
    constexpr IStringView(const char* data);
    constexpr IStringView(const IStringView& data);

    operator QByteArray() const;
    bool operator ==(const char*) const;
    bool operator ==(IStringView) const;
    bool operator !=(IStringView) const;
    bool operator <(IStringView) const;

public:
    static uint qHash(const IStringView *obj, uint seed = 0);

public:
    QString toQString() const;
    std::string toStdString() const;
    QByteArray toQByteArray() const;
    asio::const_buffer toAsioBuffer() const;

public:
    IStringView substr(const size_type _Off, size_type _Count=std::string::npos) const;
    IStringView trimmed();
    IStringViewList split(char) const;
    IStringViewList split(IStringView) const;
    bool startWith(IStringView prefix) const;
    bool endWith(IStringView suffix) const;
    bool equalIgnoreCase(IStringView) const;
    bool containIgnoreCase(IStringView) const;
};

Construction

From the above, IStringView can be constructed using types such as const std::string&, const QByteArray&, const char*, and std::string_view.

Note that one important point is that QString cannot be used to construct IStringView. This is because QString internally represents UTF-8 characters, not individual characters.

Another important point is that the lifetime of an IStringView must not exceed that of the object it is constructed from, otherwise, memory issues may occur.

Provided Functions

Conversion Functions

// Convert to QString type
QString toQString() const;

// Convert to std::string type
std::string toStdString() const;

// Convert to QByteArray type
QByteArray toQByteArray() const;

// Convert to asio::const_buffer type
asio::const_buffer toAsioBuffer() const;

The last function, asio::const_buffer, is used for converting data for transmission.

Operation Functions

// Extract a substring
IStringView substr(const size_type _Off, size_type _Count=std::string::npos) const;

// Trim whitespace characters
IStringView trimmed();

// Split the string
IStringViewList split(char) const;
IStringViewList split(IStringView) const;

// Check if the string starts with another string
bool startWith(IStringView prefix) const;

// Check if the string ends with another string
bool endWith(IStringView suffix) const;

// Check for equality, ignoring case
bool equalIgnoreCase(IStringView) const;

// Check for containment, ignoring case
bool containIgnoreCase(IStringView) const;

If users need to check for exact equality, they can use the == operator.


IStringViewList

In the split functionality above, the return type is IStringViewList. Its definition is as follows:

1
2
3
4
5
6
7
8
9
class IStringViewList : public QList<IStringView>
{
public:
    IStringViewList() = default;
    IStringViewList(QList<IStringView> data);

public:
    std::string join(IStringView);
};

This class encapsulates QList<IStringView>, providing a join function. It represents a sequence of string views.


IStringViewStash

Do you recall that the lifetime of an IStringView must not exceed that of the object it is constructed from? To solve this issue, we created the IStringViewStash object. Its essence is to pass a string object into it, cache it, and return an IStringView object. This ensures that as long as the IStringViewStash object's lifetime has not ended, the IStringView can be safely used.

Declaration

class IStringViewStash
{
public:
    IStringViewStash() = default;
    virtual ~IStringViewStash() = default;

public:
    IStringView stash(const char* data);
    IStringView stash(QByteArray&& data);
    IStringView stash(const QByteArray& data);
    IStringView stash(const QString& data);
    IStringView stash(std::string&& data);
    IStringView stash(const std::string& data);
    IStringView stash(IString&& data);
    IStringView stash(const IString& data);
    IStringView stash(IStringView data);

private:
    std::list<IString> m_stashed;
};

Construction

From the code above, it can be seen that IStringViewStash only has one function signature, the stash function. It accepts any string type and returns an IStringView type to the user. This way, the user's IStringView data does not need to worry about expiring.


Using IStringViewStash

In IWebCore, an example of using IStringViewStash is in the IHttpCore library:

1
2
3
4
5
6
7
8
9
class IHttpRequestImpl : public IStringViewStash
{
    // ...
};

class IHttpResponseRaw : public IStringViewStash
{
    // ...
};

In the example above, IHttpRequestImpl has an HTTP request lifecycle. During the request, some strings need to be cached. By directly inheriting IHttpRequestImpl from IStringViewStash, we ensure both caching capability and proper lifetime management.

Similarly, IHttpResponseRaw allows users to define multiple responses in an HTTP request, with later definitions overriding earlier ones. By caching strings, it resolves issues such as multiple copies of the same string.