在编译时截断字符串

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, …, mm ∈ ℕ 和一个数 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 的终止调用不使用 stringlength 参数,但为了兼容性必须接受它们。

由于我不明白的原因,我无法使用

std::array<CharT, N + 1> result = { chars..., 0 };
return result;

而是必须调用 workaround 辅助辅助函数。

这个解决方案有点奇怪的是,我需要 static_assertions 来确保调用正确的实例化,并且我的解决方案引入了所有这些 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>{});
}

Demo