在编译时截断字符串
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] = '[=10=]';
除了我不能使用这段代码,因为我希望 text
是静态初始化的 constexpr
。
我该怎么做?
Note: I have already found a solution to this problem but since a search on Stack Overflow did not yield me a satisfactory result (though many helpful hints for similar problems), I wanted to share my solution. If you have a better (more elegant) solution, please show it nevertheless. I will accept the most elegant answer in one week.
解决这个问题的第一步是将其形式化。给定一个字符串(字符序列)
s = s0, …, sm
with si = 0 当且仅当 i = m 对于 i = 0, …, m 和 m ∈ ℕ 和一个数 n ∈ ℕ,我们要得到另一个字符串(字符序列)
t = t0, …, tn
和
- ti = 0 如果i = n,
- ti=si 如果 i < m and
- ti = 0 否则
对于 i = 0, …, 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 功能,例如 return 类型推导和广义 constexpr
函数。
现在函数,给定 i ∈ 0, …, n,计算 ti也是直截了当的。
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),
'[=12=]'
};
它使用所需的值编译和初始化 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
'[=13=]'
};
解决此限制。 Boost.Preprocessor 库可能有助于清理这个混乱,但它不值得。使用模板元编程的更简洁的解决方案就在眼前。
让我们看看如何编写一个在编译时 return 正确初始化数组的函数。由于函数不能 return 数组,我们需要将它包装在 struct
中,但事实证明,std::array
已经为我们做了这个(以及更多),所以我们将使用它。
我用 static
函数 help
定义了一个模板助手 struct
,return 是所需的 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
辅助辅助函数。
这个解决方案有点奇怪的是,我需要 static_assert
ions 来确保调用正确的实例化,并且我的解决方案引入了所有这些 CharTs...
类型参数,而我们实际上已经知道了所有 chars...
参数的类型必须是 CharT
。
将它们放在一起,我们得到以下解决方案。
#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 <= %lu\n", std::strlen(text.data()), limit);
}
致谢 此解决方案的灵感来自于以下答案:c++11: Create 0 to N constexpr array in c++
创建 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>{});
}
我有一个字符串文字,其值超出了我的控制范围(例如 config.h
文件中的 #define
),我想初始化一个全局固定大小的字符数组它。如果字符串太长,我希望它被截断。
基本上我要达到的效果就是
#define SOMETEXT "lorem ipsum"
#define LIMIT 8
char text[LIMIT + 1];
std::strncpy(text, SOMETEXT, LIMIT);
text[LIMIT] = '[=10=]';
除了我不能使用这段代码,因为我希望 text
是静态初始化的 constexpr
。
我该怎么做?
Note: I have already found a solution to this problem but since a search on Stack Overflow did not yield me a satisfactory result (though many helpful hints for similar problems), I wanted to share my solution. If you have a better (more elegant) solution, please show it nevertheless. I will accept the most elegant answer in one week.
解决这个问题的第一步是将其形式化。给定一个字符串(字符序列)
s = s0, …, sm
with si = 0 当且仅当 i = m 对于 i = 0, …, m 和 m ∈ ℕ 和一个数 n ∈ ℕ,我们要得到另一个字符串(字符序列)
t = t0, …, tn
和
- ti = 0 如果i = n,
- ti=si 如果 i < m and
- ti = 0 否则
对于 i = 0, …, 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 功能,例如 return 类型推导和广义 constexpr
函数。
现在函数,给定 i ∈ 0, …, n,计算 ti也是直截了当的。
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),
'[=12=]'
};
它使用所需的值编译和初始化 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
'[=13=]'
};
解决此限制。 Boost.Preprocessor 库可能有助于清理这个混乱,但它不值得。使用模板元编程的更简洁的解决方案就在眼前。
让我们看看如何编写一个在编译时 return 正确初始化数组的函数。由于函数不能 return 数组,我们需要将它包装在 struct
中,但事实证明,std::array
已经为我们做了这个(以及更多),所以我们将使用它。
我用 static
函数 help
定义了一个模板助手 struct
,return 是所需的 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
辅助辅助函数。
这个解决方案有点奇怪的是,我需要 static_assert
ions 来确保调用正确的实例化,并且我的解决方案引入了所有这些 CharTs...
类型参数,而我们实际上已经知道了所有 chars...
参数的类型必须是 CharT
。
将它们放在一起,我们得到以下解决方案。
#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 <= %lu\n", std::strlen(text.data()), limit);
}
致谢 此解决方案的灵感来自于以下答案:c++11: Create 0 to N constexpr array in c++
创建 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>{});
}