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
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;
}
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;
}
I'd also add a deduction guide to be able to create fixed_str
ings 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