Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a compile time string repeating a char n times

I'm using a function like this to export data in a xml file (note: silly example):

void write_xml_file(const std::string& path)
{
    using namespace std::string_view_literals; // Use "..."sv

    FileWrite f(path);
    f<< "<root>\n"sv
     << "\t<nested1>\n"sv
     << "\t\t<nested2>\n"sv
     << "\t\t\t<nested3>\n"sv
     << "\t\t\t\t<nested4>\n"sv;
     //...
}

Where those << take a std::string_view argument:

FileWrite& FileWrite::operator<<(const std::string_view s) const noexcept
   {
    fwrite(s.data(), sizeof(char), s.length(), /* FILE* */ f);
    return *this;
   }

If necessary I can add an overload with std::string, std::array, ...

Now, I'd really love to write the above like this:

// Create a compile-time "\t\t\t..."sv
consteval std::string_view indent(const std::size_t n) { /* meh? */ }

void write_xml_file(const std::string& path)
{
    using namespace std::string_view_literals; // Use "..."sv

    FileWrite f(path);
    f<< "<root>\n"sv
     << indent(1) << "<nested1>\n"sv
     << indent(2) << "<nested2>\n"sv
     << indent(3) << "<nested3>\n"sv
     << indent(4) << "<nested4>\n"sv;
     //...
}

Is there someone that can give me an hint on how implement indent()? I'm not sure if my idea to return a std::string_view pointing to a static constant buffer allocated at compile time is the most appropriate, I'm open to other suggestions.

like image 405
MatG Avatar asked Oct 15 '25 15:10

MatG


1 Answers

If you want indent to work at compile-time, then you will require N to also be a compile time value, or for indent to be called as part of a constexpr sub-expression.

Since this is for the purpose of streaming to some file-backed stream object FileWrite, the latter is out -- which means that you need N to be at compile-time (e.g. pass it as a template argument).

This will change your signature to:

template <std::size_t N>
consteval auto indent() -> std::string_view

The second part of the problem is that you want this to return a std::string_view. The complication here is that constexpr contexts don't allow static variables -- and thus anything you create within the context will have automatic storage duration. Technically speaking, you can't just simply create an array in the function and return a string_view of it -- since this would lead to a dangling pointer (and thus UB) due to the storage going out-of-scope at the end of the function. So you will need to workaround this.

The easiest way is to use a template of a struct that holds a static array (in this case std::array so we can return it from a function):

template<std::size_t N>
struct indent_string_holder
{
    // +1 for a null-terminator. 
    // The '+1' can be removed since it's not _technically_ needed since 
    // it's a string_view -- but this can be useful for C interop.
    static constexpr std::array<char,N+1> value = make_indent_string<N>();
};

This make_indent_string<N>() is now just a simple wrapper that creates a std::array and fills it with tabs:

// Thanks to @Barry's suggestion to use 'fill' rather than
// index_sequence
template <std::size_t N>
consteval auto make_indent_string() -> std::array<char,N+1>
{
    auto result = std::array<char,N+1>{};
    result.fill('\t');
    result.back() = '\0';
    return result;
}

And then indent<N> just becomes a wrapper around the holder:

template <std::size_t N>
consteval auto indent() -> std::string_view
{ 
    const auto& str = indent_string_holder<N>::value;

    // -1 on the size if we added the null-terminator.
    // This could also be just string_view{str.data()} with the
    // terminator
    return std::string_view{str.data(), str.size() - 1u}; 
}

We can do a simple test to see if this works at compile-time, which it should:

static_assert(indent<5>() == "\t\t\t\t\t");

Live Example

If you check the assembly, you will also see that indent<5>() produces the correct compile-time string as desired:

indent_string_holder<5ul>::value:
        .asciz  "\t\t\t\t\t"

Although this works, it would actually probably be much simpler for you to write indent<N>() in terms of FileWrite (or whatever the base class is -- assuming this is ostream) instead of returning a string_view. Unless you are doing buffered writing to these streams, the cost of writing a few single characters should be minimal compared to the cost of flushing the data -- which should make this negligible.

If this is acceptible, then it would actually be much easier since you can now write it as a recursive function that passes \t to your stream object, and then calls indent<N-1>(...), e.g.:

template <std::size_t N>
auto indent(FileWrite& f) -> FileWrite&
{
    if constexpr (N > 0) {
        f << '\t'; // Output a single tab
        return indent<N-1>(f);
    }
    return f;
}

This changes the use to now be like:

FileWrite f(path);

f<< "<root>\n"sv;
indent<1>(f) << "<nested1>\n"sv;
indent<2>(f) << "<nested2>\n"sv;
indent<3>(f) << "<nested3>\n"sv;
indent<4>(f) << "<nested4>\n"sv;

But the implementation is much easier to grok and understand IMO, compared to producing a string at compile-time.

Realistically, at this point it might just be cleaner to write:

auto indent(FileWrite& f, std::size_t n) -> FileWrite&
{
    for (auto i = 0u; i < n; ++i) { f << '\t'; }
    return f;
}

which is likely what most people would expect to read; although it does come at the minimal cost of the loop (provided the optimizer does not unroll this).

like image 185
Human-Compiler Avatar answered Oct 18 '25 07:10

Human-Compiler



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!