I have this assignment for homework, which consists of me implementing a buffer optimization to a string class. I have to save the pointer from bytes 8 to 16, because the first byte is used to see if the string is heap-allocated or stack-allocated, and bytes 1-7 are used to save the length of the string. I need help finding out how I can save a pointer to dynamically allocated memory and then return it from bytes 8-16.
How I create the buffer:
alignas(CharT) std::array<std::byte, SZ> buffer;
How I want to return the buffer:
return reinterpret_cast<CharT *>(buffer[8]);
I tried to use memcpy()
and memmove()
, but when I do that I am able to return only 8 bytes from the original string.
How I create and manipulate the pointer:
auto ptr = ::operator new[](count_bytes, std::align_val_t{std::alignment_of_v<CharT>});
std::memcpy(ptr, sv.data(), count_bytes);
std::memmove(&buffer[8], ptr, sizeof(ptr));
The implementation of the buffer:
template<typename CharT = char, std::size_t SZ = 32>
struct storage {
private:
alignas(CharT) std::array<std::byte, SZ> buffer;
public:
void set(const std::basic_string_view<CharT> &sv) {
std::size_t count_bytes = sizeof(CharT) * (sv.length() + 1); ///counts the bytes of the string
auto ptr = ::operator new[](count_bytes, std::align_val_t{std::alignment_of_v<CharT>}); /// make a pointer to the memory location
std::memcpy(ptr, sv.data(), count_bytes);
std::memmove(&buffer[8], &ptr, sizeof(ptr));
operator delete[](ptr);
}
const CharT *str() {
/// Returs a pointer to the first byte of the string
return reinterpret_cast<CharT *>(buffer[8]);
}
I see several issues with your code:
There is no need to use ::operator new[]
directly, using new CharT[]
normally will suffice.
You are copying 1 too many CharT
s from sv
into the memory you are allocating.
buffer[8]
is a single byte
, whose value you are reinterpreting as a pointer, which is wrong. You need to instead reinterpret the address of that byte. If you are intending to return a CharT*
pointer to the actual bytes following your length and flag bytes (ie, for a stack-allocated string), then reinterpreting the byte address as a CharT*
is fine. But, if you are intending to return the stored CharT*
pointer (ie, to a heap-allocated string), then you need to instead reinterpret the byte address as CharT**
and dereference that pointer.
you are not storing the allocated memory length or your allocation flag into your buffer
, as you have described it must contain.
After copying the value of ptr
into your buffer
, you are calling operator delete[]
to free the memory you just allocated, thus leaving buffer
with a dangling pointer.
if the buffer
holds a pointer to dynamically allocate memory, and the set()
is called again, the previous allocated memory is leaked.
With that said, try something more like this instead:
template<typename CharT = char, std::size_t SZ = 32>
struct storage {
private:
static_assert(SZ >= (8 + sizeof(CharT*))), "SZ is too small");
struct header {
std::uint64_t flag: 8;
std::uint64_t length: 56;
union {
CharT* ptr;
std::byte data[SZ-8];
} u;
};
alignas(CharT) std::array<std::byte, SZ> buffer;
public:
void clear() {
header *hdr = reinterpret_cast<header *>(&buffer[0]);
if (hdr->flag == 1)
delete[] hdr->u.ptr;
buffer.fill(0);
}
void set(const std::basic_string_view<CharT> &sv) {
std::uint64_t len = sv.length();
if (len > 0x00FFFFFFFFFFFFFFULL) { /// make sure the length fits in 7 bytes
throw std::length_error("sv is too long in length");
}
clear();
header hdr;
hdr.flag = 1;
hdr.length = len;
hdr.u.ptr = new CharT[len+1]; /// make a pointer to the memory location
std::copy_n(sv.data(), len, hdr.u.ptr);
hdr.u.ptr[len] = static_cast<CharT>(0);
std::memcpy(&buffer, &hdr, sizeof(hdr));
}
const CharT* str() const {
/// Returns a pointer to the first byte of the string
const header *hdr = reinterpret_cast<const header *>(&buffer[0]);
if (hdr->flag == 1)
return hdr->u.ptr;
else
return reinterpret_cast<const CharT*>(hdr->u.data);
}
uint64_t length() const {
/// Returns the string length
return reinterpret_cast<const header *>(&buffer[0])->length;
}
...
};
That being said, I would take this a step further by making the flag
field be 1 bit instead of 1 whole byte, thus giving the length
field 7 more bits to work with, eg:
template<typename CharT = char, std::size_t SZ = 32>
struct storage {
private:
...
struct header {
std::uint64_t flag: 1;
std::uint64_t length: 63;
...
};
...
public:
void set(const std::basic_string_view<CharT> &sv) {
std::uint64_t len = sv.length();
if (len > std::numeric_limits<int64_t>::max()) { /// make sure the length fits in 63 bits
throw std::length_error("sv is too long in length");
}
...
}
...
};