有没有更优雅的方式在 C++ 中结合 sprintf 和 std::string?

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

在我的 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. 重新扫描可变参数有点丑陋
  2. 事实上 std::string 在内部是一个连续的字符串,space 紧跟在它后面作为空终止符,似乎实际上并没有在规范中明确说明。这暗示了 ->c_str() 必须是 O(1) 和 return 一个空终止字符串,我相信 &(data()[0]) 应该等于&(*开始())
  3. vsnprintf() 被调用两次,第一次可能会做昂贵的一次性工作

有人知道更好的方法吗?

你和 std::sprintf() 结婚了吗?如果您正在使用 C++ 和如此现代的 std::string,为什么不充分利用新的语言功能并使用可变参数模板来实现类型安全 sprintf that returns a std::string

看看这个非常漂亮的实现:https://github.com/c42f/tinyformat。我认为这可以解决您所有的问题。

不要使用大小为 0 的第一个 vsnprintf。而是使用具有一些可能大小(例如 64 到 4096 之间的东西)的堆栈缓冲区,并将其复制到 return 值中如果合适。

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

最后 - 我想重申编译时格式检查库会更好。您需要 C++14 才能发挥全部功能,但 C++11 的支持足以让您能够 #ifdef 一些头代码而不会丢失任何表现力。不过记得考虑gettext

因为你添加了标签 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 %.1f\n", "Hello", 42, 33.22 );
    return 0;
}

http://ideone.com/kSnXKj