在编译时截断字符串
Truncate a String at Compile-Time
我有一个字符串字面值,其值超出了我的控制(例如config.h
文件中的#define
),我想用它初始化一个全局固定大小的字符数组。如果字符串太长,我希望它被截断。
#define SOMETEXT "lorem ipsum"
#define LIMIT 8
char text[LIMIT + 1];
std::strncpy(text, SOMETEXT, LIMIT);
text[LIMIT] = ' ';
除了我不能使用这段代码,因为我希望text
是一个静态初始化的constexpr
。
我该怎么做?
注:我已经找到了这个问题的解决方案,但是由于堆栈溢出的搜索没有给我一个满意的结果(尽管有许多有用的提示类似的问题),我想分享我的解决方案。如果你有更好(更优雅)的解决方案,请展示出来。我将在一周内接受最优雅的答案。
创建std::array
的另一种方法:
namespace detail
{
template <typename C, std::size_t N, std::size_t...Is>
constexpr std::array<C, sizeof...(Is) + 1> truncate(const C(&s)[N], std::index_sequence<Is...>)
{
return {(Is < N ? s[Is] : static_cast<C>(0))..., static_cast<C>(0)};
}
}
template <std::size_t L, typename C, std::size_t N>
constexpr std::array<C, L + 1> truncate(const C(&s)[N])
{
return detail::truncate(s, std::make_index_sequence<L>{});
}
演示解决这个问题的第一步是将其形式化。给定一个字符串(字符序列)
<我> = <我>年代 <子> 0 子>,白马王子,,<我><子><我>我>子>我>我>我>
与<我><子><我>我>子> = 0当且仅当<我> = <我> 为<我> = 0,白马王子,<我> 和<我> 型号;& # x2115;和一个数n ∈ℕ,我们想获得另一个字符串(字符序列)我>我>我>我>我>我>
<我> t = <我><子> 0 子>,白马王子;t , <我><子><我> n 我>子>我>我>我>
- t
- <我><子><我>我>子> = 0,如果<我> = <我> n , 我>我>我> t
- <我><子><我> = <我><子><我>我>子>如果<我> & lt;<我> 和我>我>我>我>子>我> t
- <我><子><我>我>子> = 0,否则我>
for i = 0, & help;, n.
接下来,要认识到字符串的长度(在上面的形式化中是m)很容易在编译时计算出来: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;
}
我在这里使用了c++ 14的特性,如返回类型演绎和广义constexpr
函数。
现在,给定i ∈0白马王子;n <我> ,计算t <我><子><我>我>子>也很简单。我>我>
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);
}
如果我们提前知道n,我们可以用它来组合第一个快速而肮脏的解决方案:
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),
' '
};
它用所需的值编译和初始化text
,但这就是它所能说的所有优点。在每次调用char_at
时不必要地一遍又一遍地计算字符串的长度,这一事实可能是最不值得关注的。更有问题的是,如果n接近更大的值,并且常数n是隐式硬编码的,那么解决方案显然会变得非常笨拙(尽管它已经很难看了)。甚至不要考虑使用像
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
' '
};
来解决这个限制。的提振。预处理器库可能会在某种程度上帮助清理这个烂摊子,但它不值得这样做。有一个更简洁的解决方案,使用模板元编程。
让我们看看如何编写一个函数,在编译时返回正确初始化的数组。由于函数不能返回数组,我们需要将其包装在struct
中,但事实证明,std::array
已经为我们做了这些(以及更多),所以我们将使用它。
我定义了一个模板助手struct
和一个static
函数help
,它返回所需的std::array
。除了字符类型参数CharT
之外,这个struct
是基于截断字符串的长度N
(相当于上面形式化中的n)和我们已经添加的字符数M
(这与上面形式化中的变量m无关)来模板化的。
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);
}
};
可以看到,truncation_helper::help
递归地调用自己在执行过程中从要截断的字符串的前面弹出一个字符。我将字符串的长度作为附加参数传递,以避免在每次递归调用中重新计算它。
当M
到达N
时,我们通过提供部分专门化终止进程。这也是为什么我需要struct
的原因,因为函数模板不能部分特化。
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;
}
};
终止help
的调用不使用string
和length
参数,但为了兼容性必须接受它们。
由于我不明白的原因,我不能使用
std::array<CharT, N + 1> result = { chars..., 0 };
return result;
而必须调用workaround
helper-helper函数
这个解决方案的气味是,我需要static_assert
离子来确保调用正确的实例化,并且当我们实际上已经知道所有chars...
参数的类型必须是CharT
时,我的解决方案引入了所有CharTs...
类型参数。
把这些放在一起,我们得到了下面的解决方案。
#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
它可以这样使用:
#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 <= %lun", std::strlen(text.data()), limit);
}
此解决方案的灵感来自以下答案:c++11:在c++中创建0到N constexpr数组