问题描述
我正在使用这样的函数来导出 xml 文件中的数据(注意:愚蠢的例子):
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;
//...
}
那些 <<
使用 std::string_view
参数的地方:
FileWrite& FileWrite::operator<<(const std::string_view s) const noexcept
{
fwrite(s.data(),sizeof(char),s.length(),/* FILE* */ f);
return *this;
}
如有必要,我可以添加 std::string
、std::array
、...
现在,我真的很想像这样写上面的内容:
// 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;
//...
}
有人可以就如何实现 indent()
给我一个提示吗?
我不确定返回一个指向在编译时分配的静态常量缓冲区的 std::string_view
的想法是否最合适,我愿意接受其他建议。
解决方法
如果您希望 indent
在编译时工作,那么您将要求 N
也是编译时值,或 indent
作为 constexpr
子表达式的一部分调用。
由于这是为了流式传输到某些文件支持的流对象 FileWrite
,后者已失效——这意味着您需要 N
在编译时(例如传递它作为模板参数)。
这会将您的签名更改为:
template <std::size_t N>
consteval auto indent() -> std::string_view
问题的第二部分是您希望它返回一个 std::string_view
。这里的复杂之处在于 constexpr
上下文不允许 static
变量——因此您在上下文中创建的任何内容都将具有自动存储持续时间。从技术上讲,您不能简单地在函数中创建一个数组并返回它的 string_view
- 因为这会导致由于存储超出范围而导致悬空指针(从而导致 UB)在函数的最后。所以你需要解决这个问题。
最简单的方法是使用包含 template
数组的 struct
的 static
(在本例中为 std::array
,因此我们可以从函数中返回它):
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>();
};
这个 make_indent_string<N>()
现在只是一个简单的包装器,它创建一个 std::array
并用制表符填充它:
// 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;
}
然后 indent<N>
就变成了支架的包装器:
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};
}
我们可以做一个简单的测试,看看这在编译时是否有效,它应该:
static_assert(indent<5>() == "\t\t\t\t\t");
如果您检查程序集,您还会看到 indent<5>()
根据需要生成正确的编译时字符串:
indent_string_holder<5ul>::value:
.asciz "\t\t\t\t\t"
虽然这可行,但实际上,根据 indent<N>()
(或任何基类 - 假设这是 FileWrite
)来编写 ostream
可能要简单得多返回 string_view
。除非您对这些流进行缓冲写入,否则与刷新数据的成本相比,写入几个单个字符的成本应该是最小的——这应该可以忽略不计。
如果这是可以接受的,那么实际上会容易得多,因为您现在可以将其编写为将 \t
传递给您的流对象,然后调用 indent<N-1>(...)
的递归函数,例如:>
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;
}
现在的用法改成这样:
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;
但是与在编译时生成字符串相比,实现更容易理解和理解 IMO。
实际上,此时写起来可能会更简洁:
auto indent(FileWrite& f,std::size_t n) -> FileWrite&
{
for (auto i = 0u; i < n; ++i) { f << '\t'; }
return f;
}
这可能是大多数人期望阅读的内容;尽管它确实以最小的循环成本来实现(前提是优化器不展开它)。
,使用 std::string (size_t n,char c);
另见create-string-with-specified-number-of-characters
void write_xml_file(const std::string& path)
{
using namespace std::string_view_literals; // Use "..."sv
FileWrite f(path);
f<< "<root>\n"sv
<< std::string ( 1,'\t') << "<nested1>\n"sv
<< std::string ( 2,'\t') << "<nested2>\n"sv
<< std::string ( 3,'\t') << "<nested3>\n"sv
<< std::string ( 4,'\t') << "<nested4>\n"sv;
//...
}