How to avoid unnecessary stores to possibly indeterminate values in C++

2 weeks ago 18
ARTICLE AD BOX

I have an abstraction in which a byte is almost always 0, but could occasionally be indeterminate. I would like to make the byte 0 always. Once I have made the byte zero, I would like to pass my abstraction to many threads that might examine the byte, while also avoiding data races and gratuitous cache coherence misses. Here's a minimal example:

#include <cassert> #include <cstddef> #include <cstdio> #include <cstring> #include <thread> #include <vector> struct String { std::size_t size; std::size_t capacity; char *buf; template<std::size_t N> String(const char (&s)[N]) : size(N - 1), capacity(N), buf(new char[N]) { memcpy(buf, s, N - 1); } String(const String &) = delete; ~String() { delete[] buf; } const char *c_str() const { assert(size < capacity); if (buf[size] != '\0') [[unlikely]] // UB buf[size] = '\0'; return buf; } }; String greeting("hello world"); int main() { greeting.c_str(); // NUL-terminate string before spawning threads std::vector<std::jthread> greeters; for (int i = 0; i < 24; ++i) greeters.emplace_back([] { std::puts(greeting.c_str()); }); }

My problem is that buf[size] can have an indeterminate value, so evaluating buf[size] != '\0' in c_str() is undefined behavior. I can't 100% guarantee the last byte is always \0 because my real class is derived from a container with a very wide interface, so I can't guarantee that absolutely everything that happens to the bytes will leave the last byte with a '\0' value unless I've first called c_str().

The first question is for the language lawyers: Am I correct that the code is UB, and is there any way to get what I want within the language?

If strict C++ is hopeless, my code only has to work with gcc and clang, so my second question is: Can I somehow launder the pointer through a compiler intrinsic. It has to work on multiple architectures, but I'm open to laundering the byte through a vacuous asm directive that purports to put a value in the byte while in reality not containing any code. For example, would the following be safe and work on most architectures?

const char *c_str() const { assert(size < capacity); asm("" : "=m" (buf[size])); if (buf[size] != '\0') [[unlikely]] // no longer UB? buf[size] = '\0'; return buf; }

The goal here would be to import the semantics of the real machine (namely that a memory location always has a value) into the C++ abstract machine. I'm currently on C++23, but is this something that [[indeterminate]] would fix in C++26?

Read Entire Article