Constructing: Compile Time String Objects

Introduction

This post is going to demo an approach of storing string into an object and creating compile time string objects which can be evaluated at compile time in C++.
The usual approach of creating a hard-coded string in C++ is to create it as a pointer or as an array, like so:
char str* = “Test String 1”;
char str[] = “Test String2”;
This approach doesn’t really encapsulate the string into an object, and I’d be really happy if there’s a way to put this information into an object.
I’m going to use template meta-programming to achieve what I’ve planned for. By the end, we would’ve created an object that can successfully store strings and perform simple operations like compare on them. All during compilation!!!

Step-By-Step

I’m going to call my class “QCTString” and it’s going to have non-type template argument: int.
I also want a helper function to quickly create this string.

template<int N>
    class QCTString
    {
    public:
        template<int ... INDICES>
        constexpr QCTString(const char* c, <INDICES>)
        : buffer { c[INDICES]... }
        {
        }
        const char* getBuffer() const { return buffer; }
    private:
        const char buffer[N] = { '\0' };
    };
    
    template<int SZ>
    constexpr const QCTString<SZ> createCTString(const char(&str)[SZ])
    {
        return QCTString<SZ>(str, <Pass Indices somehow>);
    }

Couple of things to note in the snippet above:

  • Non-Type template parameter N represents the size of the string that’ll be stored.
  • Member variable buffer is an array of size N, with all entries initialized to ‘\0’.
  • The mysterious part in the code above is, how are we going to pass the indices? This is necessary to extract characters from the char* pointer we are passing to the constructor.
    This is what enables us to inject strings into the member variable “buffer”
  • Helper function “createCTString” does lot of work for us. The only thing we need to figure out is a way to pass indices from 0 – N(last character being \0) so that that char array we’ve passed is completely read.

To store these indices, I’m creating a proxy object that we’ll need to generate, which will have the sequence of indices. The type of this object would have variadic non-type template arguments.

template<int ... ARGS>
struct ArgsPack
{};

Once our indices are generated into ARGS, we can modify the constructor of QCTString class as follows:

template<int ... ARGS>
constexpr QCTString(const char* c, const ArgsPack<ARGS...>)
: buffer { c[ARGS]... }
{}

With this, we have simplified the problem to generating the ArgsPack<ARGS…> object.
The following snippet demonstrates how to generate the ARGS…

template<int N, int ITR, int ... ARGS>
struct IndexExtractor
{
    typedef typename IndexExtractor<N - 1, ITR + 1, ARGS..., ITR>::res res;
};

template<int ITR, int ... ARGS>
struct IndexExtractor<0, ITR, ARGS...>
{
    typedef typename ArgsPack<ARGS...> res;
};

template<int N>
struct GetIndices
{
    typedef typename IndexExtractor<N, 0>::res res;
};

This seems complicated, but is actually pretty simple. Let’s dissect the code now.

  • GetIndices is the function that will be our entry-point and which will give the final type from which we can create our ArgsPack object from.
    This initiates the type generation by initializing IndexExtractor with N and 0 and initial parameters, which represent the size left to process and current iteration respectively
  • IndexExtractor keeps calling itself, each time adding value in ITR to ARGS…, which will be passed to the next iteration.
  • We have a template specialization for IndexExtractor once N hits 0. Now, we simply return ArgsPack<ARGS…>.
    All the indices(0 – N) are nicely extracted and stored in ARGS
Conclusion

Final code would look something like this.
If you notice, I also added a compare function, which lets us compare strings at compile time!!!
How neat is that!!

/* A light-weight class that carries indices 0 - (Size - 1) individually*/
    template<int ... ARGS>
    struct ArgsPack
    {};
    
    template<int N, int ITR, int ... ARGS>
    struct IndexExtractor
    {
        typedef typename IndexExtractor<N - 1, ITR + 1, ARGS..., ITR>::res res;
    };

    template<int ITR, int ... ARGS>
    struct IndexExtractor<0, ITR, ARGS...>
    {
        typedef typename ArgsPack<ARGS...> res;
    };

    template<int N>
    struct GetIndices
    {
        typedef typename IndexExtractor<N, 0>::res res;
    };

    //TODO: Improve upon this
    /* Compile-Time string */
    template<int N>
    class QCTString
    {
    public:
        template<int ... ARGS>
        constexpr QCTString(const char* c, const ArgsPack<ARGS...>)
        : buffer { c[ARGS]... }
        {
        }

        const char* getBuffer() const { return buffer; }

        template<int M>
        constexpr bool compare(const QCTString<M>& other) const
        {
            if(N != M) return false;
            for(int i = 0; i < N; ++i)
            {
                if(buffer[i] != other.buffer[i])
                {
                    return false;
                }
            }
            return true;
        }
    private:
        const char buffer[N] = { '\0' };
    };
    
    /* Helper to create a compile-time object with given sequence of characters from input const array */
    /* Should ideally only use helpers to create QCTString Objects */
    template<int SZ>
    constexpr const QCTString<SZ> createCTString(const char(&str)[SZ])
    {
        return QCTString<SZ>(str, GetIndices<SZ>::res());
    }

This should provide you with a pretty neat abstraction for simple string operations you want to perform during compilation time.
One down-side however, is that larger and complicated strings in a large number would increase compilation time.

Hope this has been insightful.
Will see you in the next one!


Leave a Reply

Your email address will not be published. Required fields are marked *