Search code examples
c++c++20constexprconsteval

Is it possible to raise compile error when string is too long without macros in C++20 or lower?


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


Solution

  • 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