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));
}
<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
