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.
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With