C++中的命名参数字符串格式化

17
我想知道是否有像Boost Format这样的库,但支持命名参数而不是位置参数。在Python中,这是一种常见的用法,你可以使用上下文来格式化字符串,该上下文可能会使用所有可用的参数,也可能不会使用所有参数,例如:
mouse_state = {}
mouse_state['button'] = 0
mouse_state['x'] = 50
mouse_state['y'] = 30

#...

"You clicked %(button)s at %(x)d,%(y)d." % mouse_state
"Targeting %(x)d, %(y)d." % mouse_state

有没有提供类似最后两行功能的库?我期望它提供像这样的API:

PrintFMap(string format, map<string, string> args);

我在谷歌上找到了许多提供位置参数变体的库,但没有一个支持命名参数。理想情况下,该库依赖较少,以便我可以轻松地将其放入我的代码中。在C++中收集命名参数可能不太符合惯用语法,但可能有人比我更深入地思考过这个问题。

性能很重要,特别是我希望保持内存分配量较低(在C++中总是很麻烦),因为它可能在没有虚拟内存的设备上运行。但即使有一个慢的库起点,也可能比自己从头开始编写更快。


1
Boost确实有一个库可以实现第一行的功能。但是我要冒险说,如果没有一些严肃的预处理技巧,第二三行就根本不可能实现。 - Dennis Zickefoose
1
那只是我在 Python 中尝试做的一个例子,我并不期望在 C++ 中使用那种语法。而且,我非常确定我不需要 boost 来创建一个 map。;) - user79758
1
我猜如果你要处理地图而不是结构体的话,这应该是可能的,尽管我不熟悉任何已经存在的库可以这样做。 - Dennis Zickefoose
1
你的需求有点不合理:要么你可以使用这个语法糖,但相比于 printf 和其他类似函数,甚至是 Boost.Format,会导致显著的内存和处理开销;要么你可以追求性能。我认为两者都不可能兼得。 - Martin Ba
1
我不认为命名替换是语法糖 - 这就像是说一个映射是一个键值对数组的语法糖。是的,在某种程度上它们是等效的,但是其中一个的语义在许多情况下是有用的,而另一个则不是。我也不知道为什么每个人都关注语法问题,而不是是否存在任何类库做类似的事情。 - user79758
显示剩余2条评论
6个回答

12

fmt库支持命名参数:

print("You clicked {button} at {x},{y}.",
      arg("button", "b1"), arg("x", 50), arg("y", 30));

另外,你甚至可以使用用户定义的字面量来传递参数,这也是一种语法糖:

print("You clicked {button} at {x},{y}.",
      "button"_a="b1", "x"_a=50, "y"_a=30);

为了简洁起见,在上述示例中省略了命名空间fmt

免责声明:本人是该库的作者。


这并不完全相同。仅仅命名参数是不够的,还需要指定类型格式。例如,类型格式化指的是应该应用哪种类型,int/float/string等,整数/浮点类型参数前面转发多少个零,参数必须从哪个位置消耗多少个字符以及如何对齐等等。 - Andry
1
@Andry 所有常见的格式说明符都可以用于命名参数。 - vitaut
那只是一个半成品解决方案。所有的格式化都必须在单个格式字符串中一次性完成,就像这样:formatme("%{abc:02u}", FormatDic(abcvar, "abc", defaultabcvalue))。原因很简单,格式字符串可以与实际输入参数分开存储/更改。 - Andry
1
澄清一下:您可以执行 print("{abc:02u}", "abc"_a=value),它将按预期工作。格式字符串可以存储在其他地方。 - vitaut
@vitaut 你应该提到你的dynamic_format_arg_store类。 - kervin
显示剩余2条评论

7

我一直对C++的输入/输出(尤其是格式化)持有批评态度,因为我认为它相对于C而言是一步退步。格式需要是动态的,从外部资源(如文件或参数)加载它们是非常合理的。

然而,我以前从未尝试过实现其他方式,你的问题让我花费了一些周末时间来尝试这个想法。

当然,问题比我想象的更加复杂(例如仅整数格式化例程就有200多行),但是我认为这种方法(动态格式字符串)更易用。

您可以从此链接下载我的实验(仅为.h文件),并从此链接下载测试程序(“测试”可能不是正确的术语,我只是用它来查看是否能够编译)。

以下是一个示例:

#include "format.h"
#include <iostream>

using format::FormatString;
using format::FormatDict;

int main()
{
    std::cout << FormatString("The answer is %{x}") % FormatDict()("x", 42);
    return 0;
}

它与 boost.format 方法不同,因为使用了命名参数,并且格式字符串和格式字典是分开构建的(例如可以传递在不同的上下文中)。此外,我认为格式选项应该作为字符串的一部分出现(就像 printf),而不是放在代码中。
“FormatDict”的一个技巧是保持语法合理性。
FormatDict fd;
fd("x", 12)
  ("y", 3.141592654)
  ("z", "A string");

FormatString仅从const std::string&解析(我决定预解析格式字符串,但较慢但可能可接受的方法是每次传递字符串并重新解析)。

通过特化转换函数模板,可以扩展用户定义类型的格式化;例如

struct P2d
{
    int x, y;
    P2d(int x, int y)
        : x(x), y(y)
    {
    }
};

namespace format {
    template<>
    std::string toString<P2d>(const P2d& p, const std::string& parms)
    {
        return FormatString("P2d(%{x}; %{y})") % FormatDict()
            ("x", p.x)
            ("y", p.y);
    }
}

然后,可以将P2d实例简单地放置在格式化字典中。

同时,可以通过在%{之间放置参数来传递格式化函数。

目前,我只实现了整数格式化专用,支持以下功能:

  1. 左/右/居中对齐的固定大小
  2. 自定义填充字符
  3. 通用基数(2-36),小写或大写
  4. 数字分隔符(带有自定义字符和计数)
  5. 溢出字符
  6. 符号显示

我还添加了一些常见情况的快捷方式,例如

"%08x{hexdata}"

是一个带有8位数字的十六进制数,前面补零。

"%026/2,8:{bindata}"

这是一个24位二进制数(如"/2"所需),每8位带有数字分隔符":"(如",8:"所需)。

请注意,这只是一个想法的代码,例如,现在我只是防止复制,当可能允许存储格式字符串和字典时,这可能是合理的(对于字典,然而,重要的是要给予避免复制对象的能力,只因为它需要添加到FormatDict中,虽然我认为这是可能的,但这也会引起关于生命周期的非平凡问题)。

更新

我对初始方法进行了一些更改:

  1. 格式化字符串现在可以复制
  2. 自定义类型的格式化使用模板类而不是函数完成(这允许部分特化)
  3. 我添加了一个序列的格式化程序(两个迭代器)。语法仍然粗糙。

我创建了一个GitHub项目,采用boost许可证。


作为建议,所描述的代码与Python格式接近,但并不完全兼容(将s/()/{}/放在名称之前等)。通过对解析器进行一些重新编写,您可能可以使两种语言中的主要情况相同。 - BCS
我没有花太多时间考虑格式化程序的语法,例如现在我已经添加了序列,但我不喜欢我最终找到的嵌套格式字符串的方式(例如,要获取逗号空格分隔的值列表的语法是"%*/, {L}",其中*被替换为{x})。 - 6502

2
答案似乎是否定的,没有一个C++库可以做到这一点,而且根据我收到的评论,C++程序员似乎甚至没有看到这方面的需求。我将不得不再次自己编写。

实际上,我投了赞成票,因为我觉得这个问题很有趣。我编写了某种格式化程序,以一个上下文(map)作为参数,但需求大不相同:我想在不同可能的生成输出之间进行选择,而不是精确控制数字、填充、长度等的格式化。我认为从 Boost.Format 库跨越并不太重要……但问题是:你想尝试读取 boost 文件吗? - Matthieu M.
2
这不再是真的了 =)。有一个 C++ 库可以做到这一点:https://github.com/cppformat/cppformat - vitaut

1

好的,我也会添加自己的答案,虽然我不知道(或者编写)这样的库,但是我可以回答“保持内存分配下降”的问题。

像往常一样,我可以想象出某种速度/内存权衡。

一方面,您可以进行“即时解析”:

class Formater:
  def __init__(self, format): self._string = format

  def compute(self):
    for k,v in context:
      while self.__contains(k):
        left, variable, right = self.__extract(k)
        self._string = left + self.__replace(variable, v) + right

这样你就不需要保留一个“解析”结构,希望大多数时候你只需在原地插入新数据(与Python不同,C++字符串是可变的)。

然而,这远非高效...

另一方面,您可以构建一个完全构造的树来表示解析格式。您将拥有几个类,如:ConstantStringIntegerReal等,可能还有一些子类/装饰器用于格式本身。

我认为,最有效的方法应该是两者的混合。

  • 将格式字符串分解为ConstantVariable列表
  • 在另一个结构中索引变量(使用开放地址法的哈希表或类似于Loki::AssocVector的东西都可以)。

你已经完成了只使用2个动态分配的数组(基本上)的任务。如果你想允许同一个键被多次重复,只需将std::vector<size_t>作为索引值:好的实现不应该为小型向量动态分配任何内存(VC++ 2010对少于16字节的数据不进行动态分配)。

在评估上下文本身时,请查找实例。然后“即时”解析格式化程序,将其与要替换的当前值类型进行检查,并处理格式。

优缺点: - 即时:你一遍又一遍地扫描字符串 - 一次解析:需要很多专用类,可能有很多分配,但输入时验证格式。像Boost一样,它可以被重复使用。 - 混合:更有效率,特别是如果你不替换某些值(允许某种“空”值),但延迟解析格式会延迟错误报告。

个人而言,我会选择“一次解析”方案,尽可能使用boost::variant和策略模式来减少分配。


实际上,在许多平台上,你会发现任何类型的向量的堆分配成本都要比“远非高效”的解决方案大得多,后者具有重大的缓存优势。而在没有虚拟内存的平台上,即使是快速分配也会因为碎片化而变得缓慢而痛苦。 - user79758
@Joe:是的,“远非高效”这个说法有点过了。但这取决于格式的类型。如果在25个字符的字符串中只有1或2个替换,那么它将是高效的;如果在几千字节的文本中每个变量都有几十个出现,那么它会变慢。这总是效率的问题:小输入受常数影响,而大输入受大O影响 :/ - Matthieu M.

0

鉴于Python本身是用C语言编写的,并且格式化是一个常用的功能,你可能可以(忽略版权问题)从Python解释器中提取相关代码,并将其移植到使用STL映射而不是Python本地字典。


0

我为此编写了一个库,请在GitHub上查看。

欢迎贡献。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接