In Using strings in C++ template metaprograms, Abel Sinkovics and Dave Abrahams describe a set of macros to create strings for metaprogramming in C++. Since C++11 though there is another way, and with C++14 it’s made easier. This article describes that method.

I’m using C++14 features here. Namely not needing postfix decltype in auto functions. My compiler seems to have some issues with auto (it’s not the latest version) so I’m not using it everywhere I could, but where I do I’m not doing the whole -> decltype(expression) thing. It cleans up the code a LOT and is a lot simpler. You should be able to do this in C++11 though by adding those bits where necessary.

With this technique you get a string type that can be used is much easier ways for many of the same purposes. It can be used at both runtime and compile-time and at least in the cases I’ve tried and compilers I have used this version can be used with much longer strings. For example in previous attempts to implement fizzbuzz as a metaprogram I was able to only acheive very low numbers before reaching the limit of template parameters for my compiler. In the sample code for this article I acheived all 100 entries.

Prerequisite utilities: index generation

For many of the functions in this construct we need a variadic template pack of all legal index values in an array. On the stackoverflow C++ Lounge wiki a technique is described that allows this. I altered it slightly:

template < int ... Nums > struct indices { template < int I > static constexpr auto append(boost::mpl::int_<I>) { return indices<Nums...,I>(); } }; constexpr indices<> make_indices(boost::mpl::int_<0>) { return indices<>(); } template < int I > constexpr auto make_indices(boost::mpl::int_<I>) { using prev = boost::mpl::int_<I-1>; return make_indices(prev()).append(prev()); }

You can use these constructs to create a type that contains all indices for an array, and then you can use this in a pack expression to retrieve that list of indices as a variadic template pack and then use that to initialize an array.

Basic operations

Before moving on to the more interesting stuff I will briefly describe the fundamental definitions to get started with:

template < int Size > struct string { using data_type = char const (&) [Size+1]; constexpr int size() const { return Size; } constexpr data_type c_str() const { return arr; } // ... private: char arr[Size+1]; };

The most interesting thing here is the definition of data_type . For a great many things we could simply return char const* but in many cases it helps us a lot to get the raw array type out of that function. It will decay into a pointer when necessary. The rest of this stuff is pretty self explanitory: we define constexpr functions to retrieve the size and the actual string, and a place to put it.

The array needs to be Size+1 so that it can store the null terminator and thereby behave like a C string. This could be ommitted if you never wish to use it as a c-string, but in many ways it’s easier to enable that form of access.

Construction

Building this thing then becomes the first tricky part we run into. We want to be able to build this thing out of a c-style string. We also want to be able to construct a zero size string without parameters.

template < int Size > struct string { // ... constexpr string(char const (&str) [Size+1]) : string(str, detail_::make_indices(boost::mpl::int_<Size>())) {} constexpr string() : arr{} {} private: char arr[Size+1]; template < int I, int ... Indices > constexpr string(char const (&str) [I], detail_::indices<Indices...>) : arr{str[Indices]..., '\0'} {} };

Line 6 here is where interesting things start. Normally you’d not take a c-style string as an array but instead as a pointer to char. Here though we want the size of the array and in C++ strings in quotes are actually static arrays; they have a size we can fetch through the type system. To fetch that size we simply make it a template parameter. The rest is funky syntax to take an array by reference.

On line 7 we use the new delegating constructor syntax to defer to a private constructor that uses the indexing trick. So we call make_indices as the second argument to that constructor.

Line 15 begins the other side of the indexing trick. Here we say that we expect a pack of integers. On line 16 we accept the type in which that pack will expand, thus giving us the pack as part of the deduction system. Finally on line 17 we finish the indexing trick and use it to initialize our internal array.

The default constructor should probably only be available when Size==0 but I’m being lazy here. You could use enable_if to fix this or create a specialization for string<0> .

The final thing here though is that it would be nice to have a helper function so we don’t need to count characters and specify the size. It’s pretty darn easy:

template < int Size > constexpr auto make_string(char const (&cstr) [Size]) { return string<Size-1>(cstr); }

Already we have a lot of power. We can define constexpr strings quite easily and we can demonstrably use them in places that require constant expressions:

BOOST_AUTO_TEST_CASE(from_cstr) { constexpr auto s0 = make_string("hello"); BOOST_CHECK_EQUAL(s0.c_str(), "hello"); using c = boost::mpl::char_<s0.c_str()[2]>; using s = boost::mpl::int_<s0.size()>; BOOST_CHECK_EQUAL(c::value, 'l'); BOOST_CHECK_EQUAL(s::value, 5); }

Concatination

I’m going to skip over the definitions for push_front and push_back and instead just talk about append . It’s the more thorough example, leaving the other two fairly easy to understand once you can follow through append .

Like the constructor we need to use the indexing trick:

template < int Size > struct string { // ... template < int I > constexpr string<Size+I-1> append(char const (&cstr)[I]) const { return append( detail_::make_indices(boost::mpl::int_<Size>()) , detail_::make_indices(boost::mpl::int_<I-1>()) , cstr ); } // ... private: char arr[Size+1]; template < int ... ThisIndices , int ... ThatIndices , int I > constexpr string<Size+I-1> append( detail_::indices<ThisIndices...> , detail_::indices<ThatIndices...> , char const (&cstr) [I] ) const { char const newArr[] = {arr[ThisIndices]..., cstr[ThatIndices]..., '\0'}; return string<Size+I-1>(newArr); } };

It’s kind of funky looking but there’s no reason to pass on arr because we already have it. So we pass on the indices to reference both arrays and a reference to the array we want to append. We can’t just create a class variable or type to hold pre-created indices because the pack part is essential to the indexing trick and that is only available if we make it part of the function signature.

The interesting part here is on line 23 where the new c-string is created by merging the two inputs. Using the indexing trick again here twice–we can have two packs because they resolved inside clear boundaries–we just use array initialization to do so, just as with the constructor.

There’s an alternative to the newArray method. It requires a constructor that takes individual chars and it must be a variadic template. Because of this requirement I decided I didn’t much care for that constructor and so did it this way. You can see the difference in the github change history for string.hpp.

That’s basically the whole bit. It’s nice to have operators though:

template < int I0, int I1> constexpr string<I0+I1> operator + (string<I0> const& s0, string<I1> const& s1) { return s0.append(s1); }

And now we have compile-time string processing! Check out the unit test:

BOOST_AUTO_TEST_CASE(operators) { constexpr auto s0 = '[' + make_string("hello") + ' ' + make_string("world") + ']'; BOOST_CHECK_EQUAL(s0.c_str(), "[hello world]"); }

That test uses some additional operations (push_front and push_back) that are not significantly different from the append operation.

Limitations

There are a few limitations to this construct. Using them in constexpr scope will eventually hit a wall of recursion limits. I’ve found though that this is much, much greater than the MPL string.

Another limitation is that even in constexpr functions parameters are never constexpr. Thus you can’t do anything like this:

template < int I > constexpr void fun(string<I> str) { using illegal = boost::mpl::char_<str.c_str()[0]>; }

A final limitation is that different strings of the same length are not distinct types. This construct would therefor not be sufficient to implement the keys in my json library. Furthermore, because of the aforementioned issue where function parameters are never constexpr, there’s really no great way to turn them into types. Anything that might would necessarily follow the same sort of structure as Abel Sinkovics and Dave Abrahams’s version–namely it would need macros. However, for the purposes discussed in that article, such as creating a compile-time regex generator, this constexpr string type would be sufficient I’d think.

Conclusion

So, why would you use such a thing? I have no really great plan to. This was done more or less because I could and because the methods used are interesting. I’ve not initiated anything new here, but I’ve tied together a few different techniques. This string type also might be practically useful for something like the tasks mentioned in Abel Sinkovics and Dave Abrahams’s article.

As a test I did implement fizzbuzz as a constexpr string generator. I indeed hit a recursion limit wall but was able to work around it by making the string slightly shorter. It thus goes all the way from 1-100, unlike my previous attempts to create one with the MPL string where the upper limit was closer to 20 and required increasing some numeric defines before including MPL. It might be possible to remove some recursion by mixing constexpr and template metaprogramming–for example implement the index trick more like was done on the C++ Lounge site.

Obviously to make this construct practically useful in a non-painful way you’d want to add many omitted functions, the most glaring of which is an index operator.

The code for this blog is available on my github if you’ve not noticed already.

Share this: Twitter

Facebook

Like this: Like Loading... Related