Baduit

A young French developper who really likes (modern) C++

About me
10 August 2023

Generate a string at compile with C++ 17

by Baduit

Article:: Article

Not long ago, a colleague came to me with a problem: he needed to generate a string with a simple format, something like this ?,?,?.

It bothered him to generate it during runtime when he had all the needed information during compile time (Yes, we know that having to initialize one static string at the start of the program won’t be the end of the world, it’s still annoying).

To recapitulate we need to:

Let’s see the solutions I proposed.

Prerequisite to make code compile time friendly

If I want my function to be able to run at compile time, it must be constexpr (or consteval with C++20). It adds some constraint to what code I can write in it, but at each new standard the rules are a bit relaxed.

Another thing is that even if a function is constexpr, it’s argument can’t be use for a constexpr expression, meaning that this is not possible:

1
2
3
constexpr void foo(int a) {
    constexpr int b = a;
}

It means the only way I have to pass the number of times we want to repeat the pattern is to use a template parameter like this.

1
2
3
4
5
6
template <int A>
constexpr void foo() {
    constexpr int b = A;
}
// You can use this function like that:
foo<15>();

First idea

I already had this function in mind that I used in one of my pet projects to concatenate arrays at compile time

1
2
3
4
5
6
7
8
template <typename T, std::size_t aSize, std::size_t bSize>
constexpr auto concat_array(const std::array<T, aSize>& a, const std::array<T, bSize>& b)
{
	std::array<T, aSize + bSize> result_array;
	std::ranges::copy(a, result_array.begin());
	std::ranges::copy(b, result_array.begin() + aSize);
	return result_array;
}

It may seem a bit barbaric but it’s pretty simple, there is two std::array in input. It takes their size to create a new array then copy the content of both input into the new one.

It only works with std::array in input, but with a bit more code it could work with built-in arrays, I did it here if you are curious. For the return value sadly I have to use std::array because it’s not possible to return a built-in array by value.

Anyway, the goal here is just to have a proof of concept.

After that my idea was to have a function with the number of ? as a template parameter, create an array containing a ? and then recursively add ,? at each iteration. When this is over, add a \0 to terminate the string.

It gave me this code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace details {

constexpr std::array<char, 2> token_to_add = { ',' , '?'};
constexpr std::array<char, 1> endstring = { '\0'};

template <std::size_t I, std::size_t current_size>
constexpr auto concat_n_question_mark_impl(const std::array<char, current_size>& current_string) {
    if constexpr (I == 0) {
        return concat_array(current_string, endstring);
    } else {
        auto new_string = concat_array(current_string, token_to_add);
        return concat_n_question_mark_impl<I - 1>(new_string);
    }
}

}

template <std::size_t I>
constexpr auto concat_n_question_mark() {
    static_assert(I != 0, "Common don't do this");
    constexpr std::array<char, 1> start = { '?'};
    return details::concat_n_question_mark_impl<I - 1>(start);
}

Then it’s possible to use the code like this:

1
2
3
4
5
6
7
8
int main(int, char **)
{
    using namespace std::literals;

    constexpr auto str = concat_n_question_mark<5>();
    static_assert(str.data() == "?,?,?,?,?"sv);
    std::cout << str.data() << std::endl;
}

The complete code is available on compiler explorer

It works but there are (at least) two issues:

Fortunately while playing Zelda during the lunch break, I had a much simpler idea.

Final solution

The root cause of the issues in the first solution is the recursion, but I noticed that I didn’t need it. I can just calculate the size of the string I want to return from the template parameter, and I can just fill the content with a simple loop (or if you really like recursive function, a non-templated recursive function).

I got this code that you can use exactly like the previous one:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <std::size_t N>
constexpr auto concat_n_question_mark() {
    static_assert(N != 0, "Common don't do this");
    constexpr auto size = N * 2;
    constexpr auto last_char = size - 1;
    std::array<char, size> str;
    str[0] = '?';
    str[last_char] = 0;
    for (std::size_t i = 1; i < last_char; i += 2) {
        str[i] = ',';
        str[i + 1] = '?';
    }
    return str;
}

Compiler explorer link

It’s smaller and there’s less template. I also find it more expressive and readable.

Article::~Article

With some templates and constexpr function we can do a lot, with C++20 we can do even more, if I have the inspiration, I will probably make an article with a similar subject to see how we can do even more.

Sources

tags: cpp - cpp17