1

I am writing a simple fixed length string struct. In runtime, if you assign strings that are too long, I will just silently truncate them. This is for embedded and the string are to be displayed on a display of limited size, so they would get cropped one way or another.

I thought that it would be neat though, to fail compilation if one tries to assign too long string in compile time. By that I mean calling either of these:

fixed_str str = "bla bla";
fixed_str str2 {"bla bla"};
// and possibly also
fixed_str str3 = std::string_view{"bla bla"};
fixed_str str3 {std::string_view{"bla bla"}};

So the idea was something like this:


template<size_t length>
struct fixed_str final
{
  constexpr static auto max_length = length;
  static constexpr const char zero_char = 0;

  constexpr fixed_str()
  {
    chars[0] = 0;
  }

  constexpr fixed_str(const char* str)
  {
    this->operator=(str);
  }

  fixed_str& operator=(const char* str)
  {
    if (str)
    {
      auto len = std::char_traits<char>::length(str);
      auto real_len = std::min(len, length);
      if (std::is_constant_evaluated())
      {
        static_assert(len <= length, "Cannot fit the string into static buffer");
        for (size_t i = 0; i < real_len; ++i)
        {
          chars[i] = str[i];
        }
      }
      else
      {
        memcpy(chars.data(), str, real_len);
      }
      chars[real_len] = 0;
    }
    else
    {
      chars[0] = 0;
    }
    return *this;
  }

  std::array<char, length + 1> chars;

  constexpr operator std::string_view() const
  {
    return { chars.data() };
  }
}

However, it is not possible to use function argument or anything derived from it as a static_assert argument it seems. This would still fail even if I marked the overload consteval meaning it would only execute at compile time.

I was wondering if there is a way around that without macros. I did find this macro [in another answer][1]:

#include <assert.h>
#include <type_traits>

#define constexpr_assert(expression) do{if(std::is_constant_evaluated()){if(!(expression))throw 1;}else{assert(!!(expression));}}while(0)

However I dislike macros since they cannot be wrapped in namespaces and can conflict with other definitions, especially if copied from stackoverflow. Is there a trick that uses C++ directly?

I tried a few things with making a consteval function that tries to convert the argument to constant expression but I got the same errors. [1]: https://stackoverflow.com/a/76370540/607407

Tomáš Zato
  • 50,171
  • 52
  • 268
  • 778
  • 2
    For literal string constants, you can use template functions to be passed references to the arrays. Then a `static_assert` to compare its length to the `fixed_string` length. For non-literal strings it's not possible to do compile-time checking. – Some programmer dude Jul 12 '23 at 11:54
  • Add a `template fixed_str& operator=(const char(&str)[N])`? Then you can check if `N` exceeds your limit – NathanOliver Jul 12 '23 at 11:58
  • @Someprogrammerdude yes, I am only interested in literal constants, although `string_view` produced from one would be neat as well. – Tomáš Zato Jul 12 '23 at 12:03
  • Then the solution mentioned by me (and shown in part by @NathanOliver-IsonStrike) is a posibility. – Some programmer dude Jul 12 '23 at 12:05
  • @Someprogrammerdude It works indeed, but only if I do NOT implement `const char*` constructor, otherwise it will always call that one. Do you know how to ensure `const char[X]` ctor is called preferably? – Tomáš Zato Jul 12 '23 at 12:39
  • 1
    Perhaps through template specialization? See e.g. [this example](https://godbolt.org/z/vhj3K9qTK). – Some programmer dude Jul 12 '23 at 12:54

2 Answers2

1

I am only interested in literal constants

In that case, you could add a helper function to turn a string literal into a std::array<char, length + 1>:

template <std::size_t N>
    requires(N <= length + 1)  // compiletime length check
static constexpr std::array<char, length + 1> to_array(const char (&str)[N]) noexcept {
    // turn the char[N] into a std::array<char, length + 1>
    return [&str]<std::size_t... I>(std::index_sequence<I...>) {
        return std::array{(I < N ? str[I] : '\0')...};
    }(std::make_index_sequence<length + 1>());
}

With that, your constructor and assignment operator would become:

template <std::size_t N>
constexpr fixed_str(const char (&str)[N]) noexcept : chars{to_array(str)} {}

template <std::size_t N>
constexpr fixed_str& operator=(const char (&str)[N]) noexcept {
    chars = to_array(str);
    return *this;
}

Demo


If you need to combine it with taking C strings with unknown length at runtime, you could add a concept ...

template <class T>
concept char_pointer =
    std::is_pointer_v<T> &&
    std::is_same_v<std::remove_cvref_t<std::remove_pointer_t<T>>, char>;

... and with that a constructor and assignment operator:

fixed_str(char_pointer auto str) {
    std::strncpy(chars.data(), str, length);
    chars[length] = '\0';
}

fixed_str& operator=(char_pointer auto str) {
    std::strncpy(chars.data(), str, length);
    chars[length] = '\0';
    return *this;
}

Demo


I'd also add a deduction guide to be able to create fixed_strings from string literals/arrays without specifying the lengh:

// deduction guide
template<std::size_t N>
fixed_str(const char(&)[N]) -> fixed_str<N - 1>; // -1 for null terminator
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • This is much nicer than my for loop solution, thanks a lot. However I have one problem with either - the compiler seems to prefer `const char*` ctor/assignment and ignores this one if I define them. Is there a way around that? It happens even if I do not define them as `constexpr`, it will rather do it in compile time than use the `const char[x]` version. – Tomáš Zato Jul 12 '23 at 12:48
  • @TomášZato You're welcome. Did you manage to solve that using Someprogrammerdudes example? If not, I'll have a go at it in a few hours. – Ted Lyngmo Jul 12 '23 at 13:38
  • I am looking into it, but just thinking about it it seems it will lead to confusing compile errors when you try to cast something invalid. – Tomáš Zato Jul 12 '23 at 13:48
  • @TomášZato Ok, I'm back. I added a constructor and assignment operator for `(const) char*` too. What cast to something invalid do you have in mind? Perhaps you'd like some `static_assert`s in those cases? – Ted Lyngmo Jul 12 '23 at 17:49
1

I just wanted to add a C++17 variant of @TedLyngmo answer;

  template <size_t input_size, size_t... Indices>
  static constexpr auto to_array(const char(&str)[input_size], std::integer_sequence<size_t, Indices...>)
    ->container
  {
    return { (Indices < input_size ? str[Indices] : '\0')...};
  }
  template <std::size_t N>
  static constexpr container to_array(const char(&str)[N]) noexcept
  {
    static_assert(length >= N, "Cannot fit incoming string.");
    return to_array(str, std::make_integer_sequence<size_t, length>{});
  }

It works the same, that is the ctor usage is the exact same code.

Tomáš Zato
  • 50,171
  • 52
  • 268
  • 778