Search code examples
c++memorybufferdynamic-memory-allocation

Saving a void pointer in an array of bytes


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

Solution

  • 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 CharTs 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");
            }
    
            ...
        }
    
        ...
    };