The first step in solving this problem is formalizing it. Given a string (character sequence)
s = s0, …, sm
with si = 0 if and only if i = m for i = 0, …, m and m ∈ ℕ and a number n ∈ ℕ, we want to obtain another string (character sequence)
t = t0, …, tn
with
- ti = 0 if i = n,
- ti = si if i < m and
- ti = 0 otherwise
for i = 0, …, n.
Next, realize that the length of a string (m in the above formalization) is readily computed at compile-time:
template <typename CharT>
constexpr auto
strlen_c(const CharT *const string) noexcept
{
auto count = static_cast<std::size_t>(0);
for (auto s = string; *s; ++s)
++count;
return count;
}
I'm making use of C++14 features like return type deduction and generalized constexpr
functions here.
Now the function that, given i ∈ 0, …, n, computes ti is also straight-forward.
template <typename CharT>
constexpr auto
char_at(const CharT *const string, const std::size_t i) noexcept
{
return (strlen_c(string) > i) ? string[i] : static_cast<CharT>(0);
}
If we know n ahead of time, we can use this to put together a first quick-and-dirty solution:
constexpr char text[] = {
char_at(SOMETEXT, 0), char_at(SOMETEXT, 1),
char_at(SOMETEXT, 2), char_at(SOMETEXT, 3),
char_at(SOMETEXT, 4), char_at(SOMETEXT, 5),
char_at(SOMETEXT, 6), char_at(SOMETEXT, 7),
'\0'
};
It compiles and initializes text
with the desired values, but that's about all good that can be said about it. The fact that the length of the string is needlessly computed over and over again in each call to char_at
is probably the least concern. What is more problematic is that the solution (as ugly as it already is) clearly grows totally unwieldy if n approaches larger values and that the constant n is implicitly hard-coded. Don't even consider using tricks like
constexpr char text[LIMIT] = {
#if LIMIT > 0
char_at(SOMETEXT, 0),
#endif
#if LIMIT > 1
char_at(SOMETEXT, 1),
#endif
#if LIMIT > 2
char_at(SOMETEXT, 2),
#endif
// ...
#if LIMIT > N
# error "LIMIT > N"
#endif
'\0'
};
to work around this limitation. The Boost.Preprocessor library might help cleaning this mess up somewhat but it's not worth the thing. There is a much cleaner solution using template meta-programming waiting around the corner.
Let's see how we can write a function that returns the properly initialized array at compile-time. Since a function cannot return an array, we need to wrap it in a struct
but as it turns out, std::array
already does this (and more) for us, so we'll use it.
I define a template helper struct
with a static
function help
that returns the desired std::array
. Besides from the character type parameter CharT
, this struct
is templated on the length N
to which to truncate the string (equivalent to n in the above formalization) and the number M
of characters we have already added (this has nothing to do with the variable m in the above formalization).
template <std::size_t N, std::size_t M, typename CharT>
struct truncation_helper
{
template <typename... CharTs>
static constexpr auto
help(const CharT *const string,
const std::size_t length,
const CharTs... chars) noexcept
{
static_assert(sizeof...(chars) == M, "wrong instantiation");
const auto c = (length > M) ? string[M] : static_cast<CharT>(0);
return truncation_helper<N, M + 1, CharT>::help(string, length, chars..., c);
}
};
As you can see, truncation_helper::help
recursively calls itself popping one character off the front of the to-be-truncated string as it goes. I'm passing the length of the string around as an additional parameter to avoid it having to be re-computed in each recursive call anew.
We terminate the process as M
reaches N
by providing this partial specialization. This is also the reason why I need the struct
because function templates cannot be partially specialized.
template <std::size_t N, typename CharT>
struct truncation_helper<N, N, CharT>
{
template <typename... CharTs>
static constexpr auto
help(const CharT *, // ignored
const std::size_t, // ignored
const CharTs... chars) noexcept
{
static_assert(sizeof...(chars) == N, "wrong instantiation");
return truncation_helper::workaround(chars..., static_cast<CharT>(0));
}
template <typename... CharTs>
static constexpr auto
workaround(const CharTs... chars) noexcept
{
static_assert(sizeof...(chars) == N + 1, "wrong instantiation");
std::array<CharT, N + 1> result = { chars... };
return result;
}
};
The terminating invocation of help
does not use the string
and length
parameters but has to accept them nevertheless for compatibility.
For reasons I don't understand, I cannot use
std::array<CharT, N + 1> result = { chars..., 0 };
return result;
but rather have to call the workaround
helper-helper function.
What smells a little about this solution is that I need the static_assert
ions to make sure the correct instantiation is called and that my solution introduces all those CharTs...
type parameters when we actually already know that the type must be CharT
for all of the chars...
parameters.
Putting it all together, we get the following solution.
#include <array>
#include <cstddef>
namespace my
{
namespace detail
{
template <typename CharT>
constexpr auto
strlen_c(const CharT *const string) noexcept
{
auto count = static_cast<std::size_t>(0);
for (auto s = string; *s; ++s)
++count;
return count;
}
template <std::size_t N, std::size_t M, typename CharT>
struct truncation_helper
{
template <typename... CharTs>
static constexpr auto
help(const CharT *const string, const std::size_t length, const CharTs... chars) noexcept
{
static_assert(sizeof...(chars) == M, "wrong instantiation");
const auto c = (length > M) ? string[M] : static_cast<CharT>(0);
return truncation_helper<N, M + 1, CharT>::help(string, length, chars..., c);
}
};
template <std::size_t N, typename CharT>
struct truncation_helper<N, N, CharT>
{
template <typename... CharTs>
static constexpr auto
help(const CharT *, const std::size_t, const CharTs... chars) noexcept
{
static_assert(sizeof...(chars) == N, "wrong instantiation");
return truncation_helper::workaround(chars..., static_cast<CharT>(0));
}
template <typename... CharTs>
static constexpr auto
workaround(const CharTs... chars) noexcept
{
static_assert(sizeof...(chars) == N + 1, "wrong instantiation");
std::array<CharT, N + 1> result = { chars... };
return result;
}
};
} // namespace detail
template <std::size_t N, typename CharT>
constexpr auto
truncate(const CharT *const string) noexcept
{
const auto length = detail::strlen_c(string);
return detail::truncation_helper<N, 0, CharT>::help(string, length);
}
} // namespace my
It can then be used like this:
#include <cstdio>
#include <cstring>
#include "my_truncate.hxx" // suppose we've put above code in this file
#ifndef SOMETEXT
# define SOMETEXT "example"
#endif
namespace /* anonymous */
{
constexpr auto limit = static_cast<std::size_t>(8);
constexpr auto text = my::truncate<limit>(SOMETEXT);
}
int
main()
{
std::printf("text = \"%s\"\n", text.data());
std::printf("len(text) = %lu <= %lu\n", std::strlen(text.data()), limit);
}
Acknowledgments This solution was inspired by the following answer: c++11: Create 0 to N constexpr array in c++