1

I have a string literal with a value that is out of my control (for example a #define in a config.h file) and I want to initialize a global fixed-size character array with it. If the string is too long, I want it to be truncated.

Basically, what I want to achieve is the effect of

#define SOMETEXT "lorem ipsum"
#define LIMIT 8

char text[LIMIT + 1];
std::strncpy(text, SOMETEXT, LIMIT);
text[LIMIT] = '\0';

except that I cannot use this code because I want text to be a statically initialized constexpr.

How can I do this?

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.

5gon12eder
  • 24,280
  • 5
  • 45
  • 92

2 Answers2

2

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_assertions 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++

5gon12eder
  • 24,280
  • 5
  • 45
  • 92
2

An alternative to create the 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

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • That's pretty cool, definitely more elegant than mine. I would only add a `static_cast(0)` to silence the numerous (harmless) warnings. Do you know why your `return {(/* param pack expr */)..., 0};` correctly initializes the array as it ought to but I had to use the `workaround`? – 5gon12eder Jul 30 '15 at 13:04
  • @5gon12eder: I don't see why you need the `workaround` part. it works also for your code without [Here](http://ideone.com/um0IWd). – Jarod42 Jul 30 '15 at 13:33
  • Because the first `text = ""` is not exactly what I expect for `"example"` being truncated to 8 characters. – 5gon12eder Jul 30 '15 at 13:47
  • 1
    @5gon12eder: I didn't check output :-/ but gcc and clang behave differently [here](http://coliru.stacked-crooked.com/a/de9d883bfcade443) so there is a compiler bug or UB... And I failed to find UB... – Jarod42 Jul 30 '15 at 15:08
  • Thank you for looking into this. I have asked [a new question](http://stackoverflow.com/q/31762429/1392132) about it. – 5gon12eder Aug 01 '15 at 13:35