A fixed-size string buffer, String_buf (C++)

> Coding, hacking, computer graphics, game dev, and such...
User avatar
fips
Site Admin
Posts: 166
Joined: Wed Nov 12, 2008 9:49 pm
Location: Prague
Contact:

A fixed-size string buffer, String_buf (C++)

Post by fips »

I've been very satisfied with my String_ref class, which represents a non-owning reference to a string (something similar to boost::string_ref, llvm::StringRef or StringPiece), but I've soon realized that there's still one important piece missing in the puzzle. That piece is a stack-allocated string, or if you like, a fixed-size string buffer (String_buf), which would allow me to get rid of all the unnecessary heap allocations when composing (formatting) temporary strings on the stack or storing strings of a known maximum size as class members.

After a bit of googling, I've come across an interesting approach by Chromium that offers a possible solution. Namely, it's their stack_container.h, which is a custom STL allocator that allocates memory on the stack rather than on the heap. This particular implementation has a very nice ability to overflow onto the heap when the initial fixed size is exceeded - very nice and safe indeed!

Side note: I'm really happy that I don't use std::string in my interfaces any more. In this particular case the problem is that two std::strings parametrized by different STL allocators are actually two completely different types. So a string needs to get converted (copied) each time it crosses interface boundaries (even though it is passed by a const reference) . The use of String_ref in the interfaces solves this problems as the translation between different string representations is trivial!

Although a stack allocator seems attractive, I've decided to go with a custom fixed-size string buffer (String_buf) for now, mainly due to the fact that it's easily implementable using std::array. Here's a possible implementation, with a very convenient printf-like format() functionality:

VIEW THE CODE BELOW IN FULL-SCREEN (fixed_size_string_buffer_sample.cpp)

Code: Select all

/*
(c) 2013 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=1075
*/

#include <array>
#include <cassert>
#include <stdarg.h> // va_list
#define WIN32_LEAN_AND_MEAN
#include <windows.h> // _vsnprintf_s

// just to show that there's not a single heap allocation involved in this code...
void * operator new (size_t) { throw "Unexpected!"; }
void operator delete (void *) { throw "Unexpected!"; }

/// A fixed-size string buffer (typically stack-allocated).
template <size_t N>
class String_buf
{
public:

    /// Constructs from a C string if provided, or sets to "" if nullptr passed.
    String_buf(const char *cstr = nullptr)
    {
        static_assert(N > 0, "String_buf of zero size!");

        if(cstr)
        {
            assert(strlen(cstr) < N && "String_buf too small!");
            strcpy(_data.data(), cstr);
        }
        else
        {
            _data[0] = '\0';
        }
    }

    /// Constructs from a String_buf of a bigger or equal static size.
    template <size_t M>
    String_buf(const String_buf<M> &rhs)
    {
        static_assert(M <= N, "String_buf too small!");
        strcpy(_data.data(), rhs.cstr());
    }

    /// Copies from a C string if provided, or sets to "" if nullptr passed.
    String_buf & operator = (const char *cstr)
    {
        static_assert(N > 0, "String_buf of zero size!");

        if(cstr)
        {
            assert(strlen(cstr) < N && "String_buf too small!");
            strcpy(_data.data(), cstr);
        }
        else
        {
            _data[0] = '\0';
        }

        return *this;
    }

    /// Copies from a String_buf of a bigger or equal static size.
    template <size_t M>
    String_buf & operator = (const String_buf<M> &rhs)
    {
        static_assert(M <= N, "String_buf too small!");
        strcpy(_data.data(), rhs.cstr());
        return *this;
    }

    /// Returns a C string (always valid).
    const char * cstr() const { return _data.data(); }

    /// Formats a string in a good old printf style.
    void format(const char *format, ...)
    {
        va_list args;
        va_start(args, format);
        // if truncated, '\0' is automatically appended
        _vsnprintf_s(_data.data(), N, _TRUNCATE, format, args);
        va_end(args);
    }

private:

    std::array<char, N> _data;
};

int main()
{
    String_buf<8> str1 = "foo";
    String_buf<6> str2 = "bar";
    str1 = "foo2";
    str2 = "bar2";
    str1 = str2;
    //str2 = str1; // doesn't compile, static size of 'str1'<8> is bigger than 'str2'<6>!
    str2 = str1.cstr(); // this would assert if the actual size of 'str1'(4) is bigger than 'str2'<6>
    printf("%s %s\n", str1.cstr(), str2.cstr());

    String_buf<20> msg;
    msg.format("%s %s 0123456789", "Hello", "World!"); // truncated to 'Hello World! 012345'
    printf("'%s'\n", msg.cstr());

    return 0;
}

// Compiled under Visual C++ 2012, output:
// bar2 bar2
// 'Hello World! 012345'
Follow the discussion on Reddit...