我必须用sprintf格式化std::string,并将其发送到文件流。我该怎么做呢?
当前回答
这里是内存使用(和执行速度)方面的最佳解决方案,不依赖于RVO,如果字符串大小大于零,也可以执行追加,还会自动调整std::string的大小。
宏解决方案IMO更好,现代编译器将警告如果格式字符串不匹配的类型。该函数版本不会出现此警告,因为编译器无法看到snprintf。宏版本也更短,它也需要一个更少的包含。
来自:
https://github.com/ericcurtin/twincam
宏观的解决方案:
#include <string.h>
#include <string>
// function that will sprintf to a C++ string starting from std::string::size()
// so if you want to completely overwrite a string or start at a specific point
// use std::string::clear() or std::string::resize(). str is a std::string.
#define STRING_PRINTF(str, ...) \
do { \
const int size = snprintf(NULL, 0, __VA_ARGS__); \
const size_t start_of_string = str.size(); \
str.resize(start_of_string + size); \
snprintf(&str[start_of_string], str.size() + 1, __VA_ARGS__); \
} while (0)
函数的解决方案:
#include <stdarg.h> // For va_start, etc.
#include <string.h>
#include <string>
// function that will sprintf to a C++ string starting from std::string::size()
// so if you want to completely overwrite a string or start at a specific point
// use std::string::clear() or std::string::resize()
int string_printf(std::string& str, const char* const fmt, ...) {
c_va_list c_args;
va_start(c_args.args, fmt);
c_va_list tmpa;
va_copy(tmpa.args, c_args.args);
// Get addtional size required
int size = vsnprintf(NULL, 0, fmt, tmpa.args);
if (size < 0) {
return -1;
}
const size_t start_of_string = str.size();
str.resize(start_of_string + size);
// plus 1 so the null terminator gets included
size = vsnprintf(&str[start_of_string], str.size() + 1, fmt, c_args.args);
return size;
}
更优解:
#define STRING_PRINTF(str, ...) \
do { \
const size_t write_point = str.size(); \
str.resize(write_point + 127); \
const int size = snprintf(&str[write_point], 128, __VA_ARGS__); \
str.resize(write_point + size); \
if (size < 128) { \
break; \
} \
\
snprintf(&str[write_point], size + 1, __VA_ARGS__); \
} while (0)
这是一个更优的解决方案,假设sprintf小于128字节,如果是,格式字符串只解析一次而不是两次。
其他回答
现代c++使得这非常简单。
C + + 20
c++ 20引入了std::format,它允许你这样做。它使用的替换字段类似于python中的替换字段:
#include <iostream>
#include <format>
int main() {
std::cout << std::format("Hello {}!\n", "world");
}
代码来自cppreference.com, CC BY-SA和GFDL
查看编译器支持页面,看看它是否在您的标准库实现中可用。截至2021年11月28日,Visual Studio 2019 16.10(于2021年05月25日发布)和Clang 14(可在此处跟踪)提供了部分支持。在所有其他情况下,您可以求助于下面的c++ 11解决方案,或使用{fmt}库,它具有与std::format相同的语义。
C++11
使用c++ 11的std::snprintf,这已经成为一个非常简单和安全的任务。
#include <memory>
#include <string>
#include <stdexcept>
template<typename ... Args>
std::string string_format( const std::string& format, Args ... args )
{
int size_s = std::snprintf( nullptr, 0, format.c_str(), args ... ) + 1; // Extra space for '\0'
if( size_s <= 0 ){ throw std::runtime_error( "Error during formatting." ); }
auto size = static_cast<size_t>( size_s );
std::unique_ptr<char[]> buf( new char[ size ] );
std::snprintf( buf.get(), size, format.c_str(), args ... );
return std::string( buf.get(), buf.get() + size - 1 ); // We don't want the '\0' inside
}
上面的代码片段是根据CC0 1.0许可的。
逐行解释:
目的:写入一个char*使用std::snprintf,然后将其转换为std::string。
首先,使用snprintf中的一个特殊条件确定所需的char数组长度。从cppreference.com:
返回值 […如果结果字符串由于buf_size限制而被截断, 函数返回字符总数(不包括 终止空字节),如果限制为 没有实施。
这意味着所需的大小是字符数加1,因此空结束符将位于所有其他字符之后,并且可以再次被字符串构造函数截断。@alexk7在评论中解释了这个问题。
int size_s = std::snprintf( nullptr, 0, format.c_str(), args ... ) + 1;
如果发生错误,Snprintf将返回负数,因此我们随后检查格式是否按预期工作。不这样做可能会导致无声错误或分配一个巨大的缓冲区,正如@ead在评论中指出的那样。
if( size_s <= 0 ){ throw std::runtime_error( "Error during formatting." ); }
因为我们知道size_s不能为负,所以我们使用静态强制转换将size_t从有符号整型转换为无符号size_t。这样,即使是最迂腐的编译器也不会抱怨下一行可能发生的转换。
size_t size = static_cast<size_t>( size_s );
接下来,我们分配一个新的字符数组,并将其分配给std::unique_ptr。通常建议这样做,因为您不必再次手动删除它。
注意,这不是一种使用用户定义类型分配unique_ptr的安全方法,因为如果构造函数抛出异常,您就不能释放内存!
std::unique_ptr<char[]> buf( new char[ size ] );
在c++ 14中,您可以使用make_unique,这对于用户定义的类型是安全的。
auto buf = std::make_unique<char[]>( size );
在此之后,我们当然可以将snprintf用于其预期用途,并将格式化的字符串写入char[]。
std::snprintf( buf.get(), size, format.c_str(), args ... );
最后,我们创建并返回一个新的std::string,确保省略结尾的空结束符。
return std::string( buf.get(), buf.get() + size - 1 );
您可以在这里看到一个实际的例子。
如果你也想在参数列表中使用std::string,看看这个要点。
针对Visual Studio用户的其他信息:
正如回答中所解释的,微软将std::snprintf重命名为_snprintf(是的,没有std::)。MS进一步将其设置为弃用,并建议使用_snprintf_s代替,但是_snprintf_s不会接受缓冲区为零或小于格式化输出,如果发生这种情况,将不会计算输出长度。 因此,为了消除编译过程中的弃用警告,您可以在文件顶部插入以下一行,其中包含了_snprintf的使用:
#pragma warning(disable : 4996)
最终的想法
这个问题的很多答案都是在c++ 11之前编写的,并且使用固定的缓冲区长度或vargs。除非你一直使用旧版本的c++,否则我不建议你使用这些解决方案。理想情况下,走c++ 20的路。
因为这个答案中的c++ 11解决方案使用模板,如果它被大量使用,它可以生成相当多的代码。但是,除非您正在为一个二进制文件空间非常有限的环境进行开发,否则这不会成为问题,并且在清晰度和安全性方面仍然比其他解决方案有很大的改进。
如果空间效率非常重要,这两个带有vargs和vsnprintf的解决方案可能会很有用。 不要使用任何具有固定缓冲长度的解决方案,那只是在自找麻烦。
template<typename... Args>
std::string string_format(const char* fmt, Args... args)
{
size_t size = snprintf(nullptr, 0, fmt, args...);
std::string buf;
buf.reserve(size + 1);
buf.resize(size);
snprintf(&buf[0], size + 1, fmt, args...);
return buf;
}
使用C99 snprintf和c++ 11
我不喜欢把事情搞复杂。这是基于iFreilicht的答案,但我减少了一些噪音,使它更有效。请注意,如果您计划在接口中使用此功能,可能会添加一些模糊输入检查。
#include <iostream>
#include <string>
template<typename... Ts>
std::string string_format( const std::string& format, Ts... Args )
{
const size_t n = std::snprintf( nullptr, 0, format.c_str(), Args ... ) + 1; // Extra space for '\0'
std::string ret(n, '\0');
std::snprintf( &ret.front(), n, format.c_str(), Args... );
return ret;
}
int main()
{
int a = 5;
char c = 'h';
double k = 10.3;
std::cout << string_format("%d, %c, %.2f", a, c, k) << "\n";
}
输出:
5, h, 10.30
试着自己
(*唯一的警告,我发现性能方面是没有办法默认初始化字符串存储。这很遗憾,因为我们不需要在这里将所有的值初始化为“\0”。)
我知道这个问题已经被回答过很多次了,但下面这个更简洁:
std::string format(const std::string fmt_str, ...)
{
va_list ap;
char *fp = NULL;
va_start(ap, fmt_str);
vasprintf(&fp, fmt_str.c_str(), ap);
va_end(ap);
std::unique_ptr<char[]> formatted(fp);
return std::string(formatted.get());
}
例子:
#include <iostream>
#include <random>
int main()
{
std::random_device r;
std::cout << format("Hello %d!\n", r());
}
参见http://rextester.com/NJB14150
Poco Foundation库有一个非常方便的格式函数,它在格式字符串和值中都支持std::string:
道格:http://pocoproject.org/docs/Poco.html # 7308 来源:https://github.com/pocoproject/poco/blob/develop/Foundation/src/Format.cpp