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:
- generate a string composed of
?
and,
- during the compilation
- using C++17 (yeah, I know you didn't have this information so technically I'm not recapitulating)
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:
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.
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
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:
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:
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:
- It feels a bit over engineered
- With the recursion the compile times are probably really bad (I say probably because I'm too lazy to mesure it)
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:
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;
}
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.
Top comments (2)
You can initialize the array to fill it with 0 (
'\0'
would be clearer in the code) and remove some variables for a code that may be simpler:I totally agree for the initialization and for the use of \0.
For the local variable, I have no strong opinion about "size", but for "last_char" I find the line not as easy to read without it.