【问题标题】:Creating a compile time string repeating a char n times创建一个重复字符 n 次的编译时间字符串
【发布时间】:2021-09-18 00:40:24
【问题描述】:

我正在使用这样的函数将数据导出到 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;
     //...
}

那些&lt;&lt; 采用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::stringstd::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&lt;N&gt;() 更适合编译时计算。
  • consteval 是 C++20,您已将问题标记为 C++17。做出决定并更新。
  • 当然可以,前提是您在编译时使N 发生,但indent&lt;N&gt; 返回string_view 对象吗? IMO 使用 indent&lt;N&gt;(ostream&amp;) 实际上缩进流的函数可能更有意义。这也更容易编写,因为它只是递归地附加一个'\t' 并调用indent&lt;N-1&gt; 直到它到达0
  • @PaulMcKenzie: 但是std::string 无论如何不能从constexpr 方法返回。
  • @PaulMcKenzie std::string 的构造函数只有 constexpr,因此它可以在核心常量子表达式中使用,但您不能生成 constexpr string 并将其返回给非-constexpr 上下文 AFAIK。就像 constexpr auto s = std::string{2, '\t'}; is illegal in C++20 在 constexpr 上下文之外,operator&lt;&lt; for std::ostream&amp; 不符合条件。

标签: c++ c++20 string-literals compile-time string-view


【解决方案1】:

如果您希望 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),因为在函数结束。所以你需要解决这个问题。

最简单的方法是使用struct 中的template 来保存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&lt;N&gt;() 现在只是一个简单的包装器,它创建一个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&lt;N&gt; 就变成了持有者的包装:

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

Live Example

如果您检查程序集,您还将看到 indent&lt;5&gt;() 根据需要生成正确的编译时字符串:

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

虽然这可行,但实际上你可能更简单地用FileWrite(或任何基类——假设这是ostream)来编写indent&lt;N&gt;(),而不是返回string_view .除非您对这些流进行缓冲写入,否则写入几个单个字符的成本与刷新数据的成本相比应该是最小的——这应该可以忽略不计。

如果这是可以接受的,那么实际上会容易得多,因为您现在可以将其编写为递归函数,将\t 传递给您的流对象,然后调用indent&lt;N-1&gt;(...),例如:

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;
}

这可能是大多数人希望阅读的内容;尽管它确实以最小的循环成本来实现(前提是优化器没有展开它)。

【讨论】:

  • 您不需要使用index_sequence。简单得多...创建一个 char 数组和 fill() 它。
  • @Barry 很好的建议——谢谢!我真的需要更新我编写constexpr 代码的方式;我认为我受 C++11 做事方式的影响太大了
  • 感谢您的见解,我将修改您提出的两个出色的解决方案。顺便说一句,consteval 函数的参数必须在编译时知道,我想知道模板尖括号语法是否有一天会过时。
  • @MatG 尽管 consteval 函数的参数必须在编译时已知,但参数本身不被视为常量表达式(至少目前还没有)。这种区别很重要,因为这意味着您不能将该参数用作template non-type argument, or an array size 的输入。因此,至少在当前的 C++20 标准中,如果没有模板 AFAIK,就不可能在编译时生成类似 char 数组的东西。
【解决方案2】:

使用 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;
 //...
}

【讨论】:

  • 是编译时间吗?
  • 这无法按要求回答问题,因为该问题正在请求 compile-time 字符串。尽管std::string 有一个constexpr 构造函数来生成这个,std::string 本身在编译时是not;它只能在constexpr 上下文中使用——但不能逃避它。因此以这种方式使用它实际上每次都会产生一个运行时计算的字符串。
猜你喜欢
  • 1970-01-01
  • 2017-11-03
  • 2011-02-17
  • 2013-07-27
  • 2015-10-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-17
相关资源
最近更新 更多