C++ Logo

std-proposals

Advanced search

Re: [std-proposals] [Idea] Null-terminated string view (c_str_view / c_wstr_view) – a simpler alternative/complement to zstring_view

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Mon, 2 Feb 2026 23:35:32 +0000
On Mon, Feb 2, 2026 at 10:30 AM Sebastian Wittmeier via Std-Proposals
<std-proposals_at_[hidden]> wrote:
>
> No, just call a function (from a single thread) breaks it:
>
>
>
> f(ts.substr(4, 7), ts.substr(6, 2));


The debug version catches that error:

        https://godbolt.org/z/7MWb71v13

And here it is copy-pasted:

#include <cstddef> // size_t
#include <cstdio> // puts
#include <cstring> // strlen
#include <algorithm> // min
#include <stdexcept> // out_of_range

#ifndef NDEBUG
#include <cassert> // assert
#include <mutex> // lock_guard, mutex
#include <unordered_map> // unordered_map
#include <thread> // thread::id

struct Record {
    std::thread::id owner{};
    bool active = false; // true while a substring has the buffer modified
};

std::mutex records_mtx;
std::unordered_map<char const*, Record> records;

// Called when we are about to create a substring that will modify the buffer.
inline void DebugAcquire(char const* base)
{
    std::lock_guard<std::mutex> lk(records_mtx);
    auto &r = records[base];

    // Disallow any overlapping use (even same thread)
    assert(!r.active);

    r.owner = std::this_thread::get_id();
    r.active = true;
}

// Called when the substring is destroyed (buffer restored).
inline void DebugRelease(char const* base)
{
    std::lock_guard<std::mutex> lk(records_mtx);
    auto it = records.find(base);
    assert( it != records.end() );
    auto &r = it->second;

    assert(r.active);
    assert(r.owner == std::this_thread::get_id());

    r.active = false;
    r.owner = std::thread::id{};
}

// Called before reading/converting the string to ensure it isn't
currently "cut"
// by a live substring temporary.
inline void DebugAssertReadable(char const* base, bool this_instance_holds_lock)
{
    std::lock_guard<std::mutex> lk(records_mtx);
    auto &r = records[base];

    if ( this_instance_holds_lock )
    {
        // This is the live substring object that owns the cut.
        assert(r.active);
        assert(r.owner == std::this_thread::get_id());
    }
    else
    {
        // No one may read the original while a cut is active.
        assert(!r.active);
    }
}

// Called by substr() before it computes strlen / indices.
inline void DebugAssertCanCreateSubstr(char const* base)
{
    std::lock_guard<std::mutex> lk(records_mtx);
    auto &r = records[base];
    assert(!r.active);
}
#endif // ifdef NDEBUG

class TermString final {
    using size_t = std::size_t;

    char *const base; // start of the underlying buffer we’re
protecting
    char *const p; // start of this view
    size_t const cut_len = 0; // only meaningful for substring objects
    char const saved = '\0'; // char overwritten by '\0' for substring objects
#ifndef NDEBUG
    bool holds_lock = false; // true only for substring objects, for
their lifetime
#endif

    // Substring constructor: writes '\0' and holds the debug lock
until destruction.
    TermString(char *const arg_base, char *const arg_p, size_t const
len) noexcept
      : base(arg_base), p(arg_p), cut_len(len), saved(arg_p[len])
    {
#ifndef NDEBUG
        DebugAssertCanCreateSubstr(base);
        DebugAcquire(base);
        holds_lock = true;
#endif
        p[len] = '\0';
    }

public:
    // Root constructor: does NOT modify buffer.
    explicit TermString(char *const arg_p) noexcept : base(arg_p), p(arg_p) {}

    // No copying: two live handles to the same buffer is exactly what
we want to catch/avoid.
    TermString(TermString const&) = delete;
    TermString& operator=(TermString const&) = delete;
    TermString& operator=(TermString&&) = delete;

    // Move is ok: transfer "ownership" of the debug lock flag so the
moved-from dtor
    // doesn’t release it.
    TermString(TermString&& other) noexcept
      : base(other.base), p(other.p), cut_len(other.cut_len), saved(other.saved)
#ifndef NDEBUG
      , holds_lock(other.holds_lock)
#endif
    {
#ifndef NDEBUG
        other.holds_lock = false;
#endif
    }

    ~TermString(void) noexcept
    {
        // Restore buffer if this object performed a cut.
#ifndef NDEBUG
        if ( holds_lock )
        {
#endif
            p[cut_len] = saved;
#ifndef NDEBUG
            DebugRelease(base);
        }
        else
        {
            // Root object should never die while a cut is active.
            std::lock_guard<std::mutex> lk(records_mtx);
            auto &r = records[base];
            assert(!r.active);
        }
#endif
    }

    operator char const*(void) const noexcept
    {
#ifndef NDEBUG
        DebugAssertReadable(base, holds_lock);
#endif
        return p;
    }

    TermString substr(size_t const pos = 0, size_t const count = -1)
    {
        size_t const len = std::strlen(p);
        if ( pos > len ) throw std::out_of_range("out_of_range");
        size_t const n = std::min(count, len - pos);
        return TermString(base, p + pos, n);
    }
};

int main(void)
{
    char buf[] = "\\\\.\\COM1\\AES128";
    TermString ts(buf);

    using std::puts;

    puts(ts);
    puts(ts.substr(4));
    puts(ts);
    puts(ts.substr(4,4));
    puts(ts);

    printf("%s, %s", (char const*)ts.substr(4), (char const*)ts.substr(4,4));
}

Received on 2026-02-02 23:34:20