在C++中,有没有一种更优雅的方法可以将sprintf和std::string结合起来

Is there a more elegant way of marrying sprintf and std::string in C++?

本文关键字:方法 sprintf std 起来 结合 string 有没有 C++ 一种      更新时间:2023-10-16

在我的C++代码中,我经常使用以下类型的辅助函数:

static inline std::string stringf(const char *fmt, ...)
{
    std::string ret;
    // Deal with varargs
    va_list args;
    va_start(args, fmt);
    // Resize our string based on the arguments
    ret.resize(vsnprintf(0, 0, fmt, args));
    // End the varargs and restart because vsnprintf mucked up our args
    va_end(args);
    va_start(args, fmt);
    // Fill the string
    if(!ret.empty())
    {
        vsnprintf(&ret.front(), ret.size() + 1, fmt, args);
    }
    // End of variadic section
    va_end(args);
    // Return the string
    return ret;
}

它有几个优点:

  1. 字符串长度没有任意限制
  2. 字符串是在适当的位置生成的,不会被复制(如果RVO正常工作)
  3. 外界不会感到意外

现在,我有几个问题:

  1. 重新扫描varargs有点难看
  2. std::string在内部是一个连续的字符串,后面有一个null终止符的空间,这一事实似乎并没有在规范中明确说明。这是通过->c_str()必须是O(1)并返回一个null结束符的字符串来暗示的,我相信&(data()[0])应该等于&(*begin())
  3. vsnprintf()被调用两次,第一次可能会做昂贵的一次性工作

有人知道更好的方法吗?

你和std::sprintf()结婚了吗?如果你使用的是C++和非常现代的std::string,为什么不充分利用新的语言功能,使用可变模板来创建一个返回std::string的类型安全的sprintf呢?

看看这个非常好看的实现:https://github.com/c42f/tinyformat.我认为这解决了你所有的问题。

当您添加标签c++11时,我想您可以使用它。然后您可以将代码简化为:

namespace fmt {
template< class ...Args >
std::string sprintf( const char * f, Args && ...args ) {
    int size = snprintf( nullptr, 0, f, args... );
    std::string res;
    res.resize( size );
    snprintf( & res[ 0 ], size + 1, f, args... );
    return res;
}
}
int main() {
    cout << fmt::sprintf( "%s %d %.1fn", "Hello", 42, 33.22 );
    return 0;
}

http://ideone.com/kSnXKj

不要执行大小为0的第一个vsnprintf。相反,使用一个可能大小(例如64到4096之间)的堆栈缓冲区,如果合适的话,将其复制到返回值中。

你对std::string的邻接性的担忧是错误的。它定义明确,完全可以依赖。

最后,我想重申,编译时格式检查库会更好。您需要C++14才能获得完整的功能,但C++11支持的内容足够多,您应该能够在不丢失任何表现力的情况下#ifdef一些头代码。不过,记得想想gettext