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");
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).