(点击上方公众号,可快速关注)

本篇文章主要介绍现代C++字符串格式化的方法。在此之前,回顾了一些老的字符串格式化的方法,并分析各自的优劣。在最后给出了一种提供给老编译器的折中方案,因为新的格式化方法需要C++支持。

字符串格式化是极其常用的功能,这里就不多做介绍,直接进入主题。

C++20前,标准库也提供了多种的格式化方法,主要有:

C`printf`族函数

由C语言标准库引入的一组函数,典型的函数有:

int sprintf(char* buffer, const char* format, ... );     //不推荐使用 
int snprintf(char* buffer, std::size_t buf_size, const char* format, ... );

使用方式也比较直观:

int i = 3;
double d = 6.7;
const char* cs = "cs";
std::string ss = "ss";

char str[64] = { 0 };
snprintf(str, sizeof(str)-1, "i=%d, d=%-6.2f, cs=%s, ss=%s", i, d, cs, ss.c_str());

输出:

i=3, d=6.70 , cs=cs, ss=ss

需要注意下,6.70后面有两个空格,因为指定输出6个字符的宽度,而且左对齐,所以右侧空出来2个空格。

这种方式还是比较直观的,但缺点也不少:

  • 非类型安全

    参数和转换符类型不符可能导致程序崩溃;参数和转换符个数不一致也可能导致程序崩溃。。。

    由于不是类型安全的,编译器没法提前检查类型不一致的情况。估计每个人都犯过这个错:std::string类型的数据没.c_str()

  • 要求定长数组

    因为是在C中引入的,输出参数只能为定长数组,需要事先确定输出的最大长度,使用起来不方便。

  • 重复引用的参数需传入多份

    char str[64] = { 0 };
    snprintf(str, sizeof(str)-1, "i(%d) == i(%d)", i, i);
    

    格式化字符串里使用到两次i,这样参数也要传两次。工作中经常遇到类似的情况,如,组装SQL语句,导致后面参数列表特别长,可读性比较差。

C++格式化输出

C的方式不太尽如人意,我们看下C++流式的处理代码:

std::stringstream oss;
oss << "i=" << i 
    << ", d=" << std::left << std::setw(6) << std::fixed << std::setprecision(2) << d
    << ", cs=" << cs
    << ", ss=" << ss;

上面的代码与上例的代码输出完全一样。与snprintf方式相比,这里不用关心数组大小了,std::stringstream会随着内容增加容量动态增长;operator <<重载了常用的类型,也不用担心类型不匹配了,貌似很先进,但使用起来很麻烦。

从参数d的格式化看,这种方式不擅长控制复杂格式,代码简直不是给人看的。一是代码长,二是每种控制符作用范围是不同的,有的作用域整个流,有的仅影响下一个元素,反正我记了好几次,时间一长不用就忘了。

平时在工作中,简单的字符串格式化可以用用这种方法,复杂点的宁愿使用Cprintf族函数。

std::format格式化

被人诟病的字符串格式终于在C++20做出了改变,定义在<format>,它主要借鉴了python、rust等语言的格式化功能,支持的功能很多,甚至包括自定义类型的格式化、日期数据的格式化等,这里仅介绍常用的std::format函数。

template<class... Args>
std::string format(std::string_view fmt, const Args&... args);

fmt是格式字符串,std::string_view类型,不熟悉的同学可以参考之前的文章《性能控的工具箱之string_view》。args是可变的参数列表。

格式字符串的特殊字符是{},有点类似printf%,只不过这里它有两个,如果要输出这两个字符,需要分别使用转义序列 {{ 与 }} 。除此之外,正常情况下,以{开始,}结束的部分组成一个可替换域。

每个替换域的格式如下:

  • 引入的 { 字符;

  • (可选) arg-id ,非负,指代参数的索引,索引从0开始

  • (可选) 冒号( : )后随格式说明,这里不展开,可参见文章最后给出的参考资料

  • 终止的 } 字符。

上面示例改写如下:

std::format("i={}, d={:<6.2f}, cs={}, ss={}", i, d, cs, ss)

而且它也能解决参数重复的问题:

std::format("i({0}) == i({0})", i);

这里指定了arg-id,都为0,都指向了第一个参数,所以我们就不用把参数写两遍了。这里需要注意的是:要么每个替换域都指定arg-id,要么都不指定,混合使用编译器会报错。

std::format解决了printf族类型不安全的问题,也解决了流式处理可读不高的问题,是C++20推荐的字符串格式化方式。

折中方案

C++20刚出不久,截至文章发布时,主流的C++编译器只有msvc支持,需要升级到VS 2019 16.10及以上,gcc、clang目前还不支持,所以这个新的功能大家可能暂时还用不了。下面给出平时我用的折中方案,在编译器没升级之前可以先尝试用这个:

如果是C++11以上编译器,包括C++11:

template<class... T>
std::string format(const char *fmt, const T&...t)
{
    const auto len = snprintf(nullptr, 0, fmt, t...);
    std::string r;
    r.resize(static_cast<size_t>(len) + 1);
    snprintf(&r.front(), len + 1, fmt, t...);  // Bad boy
    r.resize(static_cast<size_t>(len));

    return r;
}

否则,使用:

std::string format(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    const auto len = vsnprintf(nullptr, 0, fmt, args);
    va_end(args);
    std::string r;
    r.resize(static_cast<size_t>(len) + 1);
    va_start(args, fmt);
    vsnprintf(&r.front(), len + 1, fmt, args);
    va_end(args);
    r.resize(static_cast<size_t>(len));

    return r;
}

参考资料

常见数据格式:https://en.cppreference.com/w/cpp/utility/format/formatter#Standard_format_specification

日期数据格式:https://en.cppreference.com/w/cpp/chrono/system_clock/formatter#Format_specification

喜欢我的文章,请关注我的公众号。转载请标明出处。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐