std::string 格式,如 sprintf

std::string formatting like sprintf

提问人:Max Frai 提问时间:2/26/2010 最后编辑:DeduplicatorMax Frai 更新时间:7/18/2023 访问量:1306963

问:

我必须使用 sprintf 格式化 std::string 并将其发送到文件流中。我该怎么做?

C++ 字符串 格式 stdstring c++-标准库

评论

1赞 Douglas Daseeco 9/22/2018
@Ockonal — 为了社区(我不能不关心我的代表),我建议你改变你的选择。在第一个代码段中,当前选择的代码在使用任意最大长度时呈现了一个等待发生的错误。第二个片段完全忽略了你所说的使用 vargs 的愿望,比如 sprintf。我建议你在这里选择唯一干净、安全、仅依赖 C++ 标准、经过测试和良好评论的答案。它是我的无关紧要。这是客观事实。请参见 stackoverflow.com/questions/2342162/...
0赞 Ciro Santilli OurBigBook.com 7/31/2019
@TrevorBoydSmith a 已添加到 C++20 顺便说一句:stackoverflow.com/a/57286312/895245 太棒了!std::format
4赞 Trevor Boyd Smith 7/31/2019
@CiroSantilli我昨天读了一篇文章,我看到它被复制了(现在是第一百万次)通过添加到规范中!我非常非常高兴!在过去的 9 年里,我写的几乎每个 C++ 文件都使用了 .在 C++ 中将官方 printf 样式输出添加到流中将大大有助于 IMO 的所有 C++。C++20C++20booststd::formatC++20boost::format
5赞 iFreilicht 10/15/2020
@TrevorBoydSmith 这是不正确的。 使用替换字段 (),而不是 和 的 -样式。这最初是由 Victor Zverovich 在库中实现的,他还编写了 C++ 20,并且在这个线程中有一个答案std::format{}%printfboost::format{fmt}std::format

答:

412赞 Doug T. 2/26/2010 #1

你不能直接执行此操作,因为你没有对底层缓冲区的写入权限(直到 C++11;请参阅 Dietrich Epp 的评论)。你必须先在 c-string 中执行此操作,然后将其复制到 std::string 中:

  char buff[100];
  snprintf(buff, sizeof(buff), "%s", "Hello");
  std::string buffAsStdStr = buff;

但我不确定你为什么不直接使用字符串流?我假设你有具体的理由不这样做:

  std::ostringstream stringStream;
  stringStream << "Hello";
  std::string copyOfStr = stringStream.str();

评论

21赞 John Dibling 2/26/2010
神奇的饼干使这个解决方案不是很强大。但基本思想就在那里。char buf[100];
20赞 Jimbo 1/21/2013
约翰,溪流并不慢。流看起来很慢的唯一原因是,默认情况下,iostreams 与 C FILE 输出同步,以便正确输出混合的 cout 和 printfs。禁用此链接(调用 cout.sync_with_stdio(false))会导致 c++ 的流性能优于 stdio,至少从 MSVC10 开始是这样。
80赞 Martijn Courteaux 7/9/2013
使用格式的原因是让本地化人员重建外语句子的结构,而不是对句子的语法进行硬编码。
282赞 quant_dev 4/5/2015
出于某种原因,其他语言使用类似 printf 的语法:Java、Python(新语法仍然更接近 printf 而不是流)。只有C++对无辜的人类施加了这种冗长的可憎之物。
14赞 Aaron McDaid 7/10/2015
更好的是,使用 ,它分配一个具有足够空间来保存结果的新字符串。然后,如果您愿意,请将其复制到原件中,并记住原件。此外,可以将其放在宏中,以便任何好的编译器都可以帮助您验证格式 - 您不想将 a 放在预期的地方asprintfstd::stringfreedouble%s
3赞 Hassan Syed 2/26/2010 #2

string 没有你需要的东西,但 std::stringstream 有。使用 stringstream 创建字符串,然后提取字符串。以下是您可以做的事情的完整列表。例如:

cout.setprecision(10); //stringstream is a stream like cout

在打印双精度或浮点数时,将为您提供 10 位小数的精度。

评论

8赞 Erik Aronesty 8/27/2014
这仍然没有给你任何东西接近 printf 给你的控制......但很好。
18赞 Timo Geusch 2/26/2010 #3

如果你只想要一个类似 printf 的语法(不自己调用 printf),请查看 Boost Format

评论

0赞 Douglas Daseeco 7/24/2018
为这么简单的事情添加一个完整的库是不方便的。这在 stackoverflow.com/questions/19009094/ 得到了回答......
120赞 kennytm 2/26/2010 #4

boost::format() 提供了您想要的功能:

根据 Boost 格式库概要:

格式对象由格式字符串构造,然后通过重复调用运算符来提供参数。 然后,这些参数中的每一个都转换为字符串,这些字符串又根据格式字符串组合成一个字符串。

#include <boost/format.hpp>

cout << boost::format("writing %1%,  x=%2% : %3%-th try") % "toto" % 40.23 % 50; 
// prints "writing toto,  x=40.230 : 50-th try"

评论

5赞 Hassan Syed 2/27/2010
您也可以从 Boost 中修剪所需的库。使用辅助工具。
10赞 vitaut 5/20/2014
Boost Format 不仅大,而且速度很慢。参见 zverovich.net/2013/09/07/... 和 boost.org/doc/libs/1_52_0/libs/spirit/doc/html/spirit/karma/...
20赞 quant_dev 4/5/2015
在项目中的任何位置都包含 boost 会立即大大增加编译时间。对于大型项目,这很可能无关紧要。对于小型项目,提升是一种拖累。
3赞 AturSams 8/17/2015
@vitaut 虽然与替代品相比,它非常耗费资源。您多久格式化一次字符串?考虑到它只需要几微秒,而且大多数项目可能只使用了几十次,在一个不太关注字符串格式的项目中,这并不明显,对吧?
2赞 xor007 10/26/2015
不幸的是,boost::format 的工作方式不同:不接受var_args。有些人喜欢让与单个程序相关的所有代码看起来都一样/使用相同的习语。
15赞 slashmais 9/19/2010 #5

[编辑:20/05/25] 更好的是...: 在标题中:

// `say` prints the values
// `says` returns a string instead of printing
// `sayss` appends the values to it's first argument instead of printing
// `sayerr` prints the values and returns `false` (useful for return statement fail-report)<br/>

void PRINTSTRING(const std::string &s); //cater for GUI, terminal, whatever..
template<typename...P> void say(P...p) { std::string r{}; std::stringstream ss(""); (ss<<...<<p); r=ss.str(); PRINTSTRING(r); }
template<typename...P> std::string says(P...p) { std::string r{}; std::stringstream ss(""); (ss<<...<<p); r=ss.str(); return r; }
template<typename...P> void sayss(std::string &s, P...p) { std::string r{}; std::stringstream ss(""); (ss<<...<<p); r=ss.str();  s+=r; } //APPENDS! to s!
template<typename...P> bool sayerr(P...p) { std::string r{}; std::stringstream ss("ERROR: "); (ss<<...<<p); r=ss.str(); PRINTSTRING(r); return false; }

-函数是用来满足GUI或终端或任何特殊输出需求的,默认为:PRINTSTRING(r)#ifdef _some_flag_

void PRINTSTRING(const std::string &s) { std::cout << s << std::flush; }

[编辑 '17/8/31]添加可变参数模板版本“vtspf(..)”:

template<typename T> const std::string type_to_string(const T &v)
{
    std::ostringstream ss;
    ss << v;
    return ss.str();
};

template<typename T> const T string_to_type(const std::string &str)
{
    std::istringstream ss(str);
    T ret;
    ss >> ret;
    return ret;
};

template<typename...P> void vtspf_priv(std::string &s) {}

template<typename H, typename...P> void vtspf_priv(std::string &s, H h, P...p)
{
    s+=type_to_string(h);
    vtspf_priv(s, p...);
}

template<typename...P> std::string temp_vtspf(P...p)
{
    std::string s("");
    vtspf_priv(s, p...);
    return s;
}

这实际上是有时阻碍 -运算符的逗号分隔版本(而不是),使用方式如下:<<

char chSpace=' ';
double pi=3.1415;
std::string sWorld="World", str_var;
str_var = vtspf("Hello", ',', chSpace, sWorld, ", pi=", pi);


[编辑]改编为利用 Erik Aronesty 的回答(上图)中的技术:

#include <string>
#include <cstdarg>
#include <cstdio>

//=============================================================================
void spf(std::string &s, const std::string fmt, ...)
{
    int n, size=100;
    bool b=false;
    va_list marker;

    while (!b)
    {
        s.resize(size);
        va_start(marker, fmt);
        n = vsnprintf((char*)s.c_str(), size, fmt.c_str(), marker);
        va_end(marker);
        if ((n>0) && ((b=(n<size))==true)) s.resize(n); else size*=2;
    }
}

//=============================================================================
void spfa(std::string &s, const std::string fmt, ...)
{
    std::string ss;
    int n, size=100;
    bool b=false;
    va_list marker;

    while (!b)
    {
        ss.resize(size);
        va_start(marker, fmt);
        n = vsnprintf((char*)ss.c_str(), size, fmt.c_str(), marker);
        va_end(marker);
        if ((n>0) && ((b=(n<size))==true)) ss.resize(n); else size*=2;
    }
    s += ss;
}

[上一个答案]
一个很晚的答案,但对于那些像我一样喜欢“sprintf”方式的人来说:我已经编写并使用了以下函数。如果你喜欢它,你可以扩展 %-options 以更接近 sprintf 选项;目前那里的那些足以满足我的需求。 你使用 stringf() 和 stringfappend() 与 sprintf 相同。请记住,...必须是 POD 类型。

//=============================================================================
void DoFormatting(std::string& sF, const char* sformat, va_list marker)
{
    char *s, ch=0;
    int n, i=0, m;
    long l;
    double d;
    std::string sf = sformat;
    std::stringstream ss;

    m = sf.length();
    while (i<m)
    {
        ch = sf.at(i);
        if (ch == '%')
        {
            i++;
            if (i<m)
            {
                ch = sf.at(i);
                switch(ch)
                {
                    case 's': { s = va_arg(marker, char*);  ss << s;         } break;
                    case 'c': { n = va_arg(marker, int);    ss << (char)n;   } break;
                    case 'd': { n = va_arg(marker, int);    ss << (int)n;    } break;
                    case 'l': { l = va_arg(marker, long);   ss << (long)l;   } break;
                    case 'f': { d = va_arg(marker, double); ss << (float)d;  } break;
                    case 'e': { d = va_arg(marker, double); ss << (double)d; } break;
                    case 'X':
                    case 'x':
                        {
                            if (++i<m)
                            {
                                ss << std::hex << std::setiosflags (std::ios_base::showbase);
                                if (ch == 'X') ss << std::setiosflags (std::ios_base::uppercase);
                                char ch2 = sf.at(i);
                                if (ch2 == 'c') { n = va_arg(marker, int);  ss << std::hex << (char)n; }
                                else if (ch2 == 'd') { n = va_arg(marker, int); ss << std::hex << (int)n; }
                                else if (ch2 == 'l') { l = va_arg(marker, long);    ss << std::hex << (long)l; }
                                else ss << '%' << ch << ch2;
                                ss << std::resetiosflags (std::ios_base::showbase | std::ios_base::uppercase) << std::dec;
                            }
                        } break;
                    case '%': { ss << '%'; } break;
                    default:
                    {
                        ss << "%" << ch;
                        //i = m; //get out of loop
                    }
                }
            }
        }
        else ss << ch;
        i++;
    }
    va_end(marker);
    sF = ss.str();
}

//=============================================================================
void stringf(string& stgt,const char *sformat, ... )
{
    va_list marker;
    va_start(marker, sformat);
    DoFormatting(stgt, sformat, marker);
}

//=============================================================================
void stringfappend(string& stgt,const char *sformat, ... )
{
    string sF = "";
    va_list marker;
    va_start(marker, sformat);
    DoFormatting(sF, sformat, marker);
    stgt += sF;
}

评论

0赞 slashmais 3/12/2013
@MooingDuck:根据 Dan 对 Aronesty 回答的评论更改了函数参数。我只使用 Linux/gcc,作为参考,它工作正常。(但我想人们会想玩玩具,所以......如果还有其他所谓的“错误”,您能详细说明一下吗?fmt
0赞 Mooing Duck 3/13/2013
我误解了他的部分代码是如何工作的,并认为它对许多大小进行了调整。重新检查表明我错了。您的代码是正确的。
0赞 Douglas Daseeco 9/22/2018
建立在埃里克·阿罗内斯蒂(Erik Aronesty)的答案之上的是一条红鲱鱼。他的第一个代码示例不安全,第二个代码示例效率低下且笨拙。如果任何 vprintf 系列函数的buf_siz为零,则不会写入任何内容,并且缓冲区可能是空指针,但仍会计算并返回返回值(将写入的字节数,不包括空终止符)。生产质量的答案在这里:stackoverflow.com/questions/2342162/......
0赞 asad_nitp 11/23/2020
%g呢?
251赞 21 revs, 14 users 43%Erik Aronesty #6

内部使用的 C++ 11 解决方案:vsnprintf()

#include <stdarg.h>  // For va_start, etc.

std::string string_format(const std::string fmt, ...) {
    int size = ((int)fmt.size()) * 2 + 50;   // Use a rubric appropriate for your code
    std::string str;
    va_list ap;
    while (1) {     // Maximum two passes on a POSIX system...
        str.resize(size);
        va_start(ap, fmt);
        int n = vsnprintf((char *)str.data(), size, fmt.c_str(), ap);
        va_end(ap);
        if (n > -1 && n < size) {  // Everything worked
            str.resize(n);
            return str;
        }
        if (n > -1)  // Needed size returned
            size = n + 1;   // For null char
        else
            size *= 2;      // Guess at a larger size (OS specific)
    }
    return str;
}

一种更安全、更高效(我测试过,而且速度更快)的方法:

#include <stdarg.h>  // For va_start, etc.
#include <memory>    // For std::unique_ptr

std::string string_format(const std::string fmt_str, ...) {
    int final_n, n = ((int)fmt_str.size()) * 2; /* Reserve two times as much as the length of the fmt_str */
    std::unique_ptr<char[]> formatted;
    va_list ap;
    while(1) {
        formatted.reset(new char[n]); /* Wrap the plain char array into the unique_ptr */
        strcpy(&formatted[0], fmt_str.c_str());
        va_start(ap, fmt_str);
        final_n = vsnprintf(&formatted[0], n, fmt_str.c_str(), ap);
        va_end(ap);
        if (final_n < 0 || final_n >= n)
            n += abs(final_n - n + 1);
        else
            break;
    }
    return std::string(formatted.get());
}

按值传递,以符合 的要求。fmt_strva_start

注意:“更安全”和“更快”的版本在某些系统上不起作用。因此,两者仍然被列出。此外,“更快”完全取决于预分配步骤是否正确,否则会使其变慢。strcpy

评论

3赞 0xDEAD BEEF 5/18/2012
慢。为什么要将大小增加 1?这个函数什么时候返回 -1?
28赞 quantum 8/22/2012
您正在覆盖 str.c_str()?这不是很危险吗?
8赞 Steve Hanov 8/28/2012
带有引用参数va_start在 MSVC 上存在问题。它以静默方式失败,并返回指向随机内存的指针。解决方法是使用 std::string fmt 而不是 std::string &fmt,或编写包装器对象。
6赞 Doug T. 9/25/2012
我+1,因为我知道这可能会根据大多数std::strings的实现方式起作用,但是c_str并不是真正打算修改底层字符串的地方。它应该是只读的。
6赞 Masood Khaari 5/15/2013
要事先获取生成的字符串长度,请参阅: stackoverflow.com/a/7825892/908336 我看不出在每次迭代中增加有什么意义,而您可以通过第一次调用 来获取它。sizevsnprintf()
2赞 Dave 2/26/2012 #7

这是我在程序中用于执行此操作的代码......这没什么花哨的,但它确实做到了......请注意,您必须根据需要调整尺寸。对我来说MAX_BUFFER是 1024。

std::string Format ( const char *fmt, ... )
{
    char textString[MAX_BUFFER*5] = {'\0'};

    // -- Empty the buffer properly to ensure no leaks.
    memset(textString, '\0', sizeof(textString));

    va_list args;
    va_start ( args, fmt );
    vsnprintf ( textString, MAX_BUFFER*5, fmt, args );
    va_end ( args );
    std::string retStr = textString;
    return retStr;
}

评论

4赞 EricSchaefer 3/3/2012
textString 的初始化已将整个缓冲区设置为零。无需记忆...
0赞 Mooing Duck 3/13/2013
这是对数据进行完整的额外复制,可以直接在字符串中使用。vsnprintf
20赞 Piti Ongmongkolkul 4/14/2012 #8

我使用 vsnprintf 编写了自己的字符串,因此它返回字符串,而不必创建自己的缓冲区。

#include <string>
#include <cstdarg>

//missing string printf
//this is safe and convenient but not exactly efficient
inline std::string format(const char* fmt, ...){
    int size = 512;
    char* buffer = 0;
    buffer = new char[size];
    va_list vl;
    va_start(vl, fmt);
    int nsize = vsnprintf(buffer, size, fmt, vl);
    if(size<=nsize){ //fail delete buffer and try again
        delete[] buffer;
        buffer = 0;
        buffer = new char[nsize+1]; //+1 for /0
        nsize = vsnprintf(buffer, size, fmt, vl);
    }
    std::string ret(buffer);
    va_end(vl);
    delete[] buffer;
    return ret;
}

所以你可以像这样使用它

std::string mystr = format("%s %d %10.5f", "omg", 1, 10.5);

评论

0赞 Mooing Duck 3/13/2013
这是对数据进行完整的额外复制,可以直接在字符串中使用。vsnprintf
1赞 Masood Khaari 5/15/2013
使用 stackoverflow.com/a/7825892/908336 中的代码事先获取生成的字符串长度。您可以将智能指针用于异常安全代码:std::unique_ptr<char[]> buffer (new char[size]);
2赞 Josh Haberman 11/3/2013
我不确定这在回退情况下是否正确;我认为您需要对第二个 vsnprintf() 进行 vl va_copy才能正确查看参数。有关示例,请参阅:github.com/haberman/upb/blob/...
0赞 Swiss Frank 10/29/2022
如果不再次调用 va_end() 和 va_start(),则无法重用 vl。在两个 vsnprintf 和 之后添加一个 va_end(),并在第二个之前添加一个va_start。
3赞 EddieV223 1/17/2013 #9

你可以试试这个:

string str;
str.resize( _MAX_PATH );

sprintf( &str[0], "%s %s", "hello", "world" );
// optionals
// sprintf_s( &str[0], str.length(), "%s %s", "hello", "world" ); // Microsoft
// #include <stdio.h>
// snprintf( &str[0], str.length(), "%s %s", "hello", "world" ); // c++11

str.resize( strlen( str.data() ) + 1 );
5赞 ChetS 2/2/2013 #10

根据 Erik Aronesty 提供的答案:

std::string string_format(const std::string &fmt, ...) {
    std::vector<char> str(100,'\0');
    va_list ap;
    while (1) {
        va_start(ap, fmt);
        auto n = vsnprintf(str.data(), str.size(), fmt.c_str(), ap);
        va_end(ap);
        if ((n > -1) && (size_t(n) < str.size())) {
            return str.data();
        }
        if (n > -1)
            str.resize( n + 1 );
        else
            str.resize( str.size() * 2);
    }
    return str.data();
}

这避免了抛弃原始答案中结果的需要。const.c_str()

评论

1赞 Douglas Daseeco 9/22/2018
建立在埃里克·阿罗内斯蒂(Erik Aronesty)的答案之上的是一条红鲱鱼。他的第一个代码示例是不安全的,而他的第二个代码示例,带有循环,效率低下且笨拙。如果任何 vprintf 系列函数的buf_siz为零,则不会写入任何内容,并且缓冲区可能是空指针,但仍会计算并返回返回值(将写入的字节数,不包括空终止符)。生产质量的答案在这里:stackoverflow.com/questions/2342162/......
0赞 ChetS 10/6/2018
埃里克·阿罗内斯蒂(Erik Aronesty)的答案自从添加以来已被编辑。我想强调一下使用 vector<char> 在构建字符串时存储字符串的选项。从 C++ 代码调用 C 函数时,我经常使用这种技术。有趣的是,这个问题现在有 34 个答案。
0赞 ChetS 10/12/2018
vfprintf 页面上的 cppreference.com 示例是稍后添加的。我相信最好的答案是目前接受的答案,使用字符串流而不是 printf 变体是 C++ 的方式。然而,我的回答在提供时确实增加了价值;它比当时的其他答案要好得多。现在该标准有string_view、参数包和 Variadic 模板,新的答案可能包括这些功能。至于我的回答,虽然它可能不再值得额外的赞成票,但它不值得被删除或反对,所以我保持原样。
11赞 PW. 4/10/2013 #11

谷歌就是这样做的:StringPrintf(BSD许可证)和Facebook以非常相似的方式做到这一点:StringPrintf(Apache许可证)

两者都提供了一个方便的。
StringAppendF

5赞 pixelpoint 1/13/2014 #12
inline void format(string& a_string, const char* fmt, ...)
{
    va_list vl;
    va_start(vl, fmt);
    int size = _vscprintf( fmt, vl );
    a_string.resize( ++size );
    vsnprintf_s((char*)a_string.data(), size, _TRUNCATE, fmt, vl);
    va_end(vl);
}

评论

2赞 Dacav 10/5/2014
+1 表示聪明的想法,但还不清楚是什么。我认为你应该详细说明这个答案。_vscprintf
1赞 riot_starter 5/30/2014 #13

Poco Foundation 库有一个非常方便的格式函数,它支持 std::string 格式字符串和值:

1赞 vinkris 8/4/2014 #14

您可以使用 iomanip 头文件格式化 cout 中的 C++ 输出。 在使用任何帮助程序函数(如 setprecision、setfill 等。

这是我过去用来打印矢量中平均等待时间的代码片段,我已经“累积”了。

#include<iomanip>
#include<iostream>
#include<vector>
#include<numeric>

...

cout<< "Average waiting times for tasks is " << setprecision(4) << accumulate(all(waitingTimes), 0)/double(waitingTimes.size()) ;
cout << " and " << Q.size() << " tasks remaining" << endl;

以下是我们如何格式化 C++ 流的简要说明。http://www.cprogramming.com/tutorial/iomanip.html

1赞 Valdemar_Rudolfovich 8/17/2014 #15

如果缓冲区不够大,无法打印字符串,则可能会出现问题。在其中打印格式化邮件之前,必须确定格式化字符串的长度。 我为此制作了自己的助手(在 Windows 和 Linux GCC 上进行了测试),您可以尝试使用它。

字符串.cpp:http://pastebin.com/DnfvzyKP
String.h:http://pastebin.com/7U6iCUMa

字符串 .cpp:

#include <cstdio>
#include <cstdarg>
#include <cstring>
#include <string>

using ::std::string;

#pragma warning(disable : 4996)

#ifndef va_copy
#ifdef _MSC_VER
#define va_copy(dst, src) dst=src
#elif !(__cplusplus >= 201103L || defined(__GXX_EXPERIMENTAL_CXX0X__))
#define va_copy(dst, src) memcpy((void*)dst, (void*)src, sizeof(*src))
#endif
#endif

///
/// \breif Format message
/// \param dst String to store formatted message
/// \param format Format of message
/// \param ap Variable argument list
///
void toString(string &dst, const char *format, va_list ap) throw() {
  int length;
  va_list apStrLen;
  va_copy(apStrLen, ap);
  length = vsnprintf(NULL, 0, format, apStrLen);
  va_end(apStrLen);
  if (length > 0) {
    dst.resize(length);
    vsnprintf((char *)dst.data(), dst.size() + 1, format, ap);
  } else {
    dst = "Format error! format: ";
    dst.append(format);
  }
}

///
/// \breif Format message
/// \param dst String to store formatted message
/// \param format Format of message
/// \param ... Variable argument list
///
void toString(string &dst, const char *format, ...) throw() {
  va_list ap;
  va_start(ap, format);
  toString(dst, format, ap);
  va_end(ap);
}

///
/// \breif Format message
/// \param format Format of message
/// \param ... Variable argument list
///
string toString(const char *format, ...) throw() {
  string dst;
  va_list ap;
  va_start(ap, format);
  toString(dst, format, ap);
  va_end(ap);
  return dst;
}

///
/// \breif Format message
/// \param format Format of message
/// \param ap Variable argument list
///
string toString(const char *format, va_list ap) throw() {
  string dst;
  toString(dst, format, ap);
  return dst;
}


int main() {
  int a = 32;
  const char * str = "This works!";

  string test(toString("\nSome testing: a = %d, %s\n", a, str));
  printf(test.c_str());

  a = 0x7fffffff;
  test = toString("\nMore testing: a = %d, %s\n", a, "This works too..");
  printf(test.c_str());

  a = 0x80000000;
  toString(test, "\nMore testing: a = %d, %s\n", a, "This way is cheaper");
  printf(test.c_str());

  return 0;
}

字符串.h:

#pragma once
#include <cstdarg>
#include <string>

using ::std::string;

///
/// \breif Format message
/// \param dst String to store formatted message
/// \param format Format of message
/// \param ap Variable argument list
///
void toString(string &dst, const char *format, va_list ap) throw();
///
/// \breif Format message
/// \param dst String to store formatted message
/// \param format Format of message
/// \param ... Variable argument list
///
void toString(string &dst, const char *format, ...) throw();
///
/// \breif Format message
/// \param format Format of message
/// \param ... Variable argument list
///
string toString(const char *format, ...) throw();

///
/// \breif Format message
/// \param format Format of message
/// \param ap Variable argument list
///
string toString(const char *format, va_list ap) throw();

评论

0赞 drwatsoncode 12/22/2016
关于行 -- 假设字符串的缓冲区有终止 null 字符的空间是否安全?是否有不分配 size+1 字符的实现。这样做会更安全吗vsnprintf((char *)dst.data(), dst.size() + 1, format, ap);dst.resize(length+1); vsnprintf((char *)dst.data(), dst.size(), format, ap); dst.resize(length);
0赞 drwatsoncode 12/22/2016
显然,我之前评论的答案是:不,假设存在空字符是不安全的。特别是关于C++98规范:“访问data()+size()处的值会产生未定义的行为不能保证空字符终止此函数返回的值指向的字符序列。请参阅 string::c_str 以获取提供此类保证的函数。程序不得更改此序列中的任何字符。但是,C++11 规范表明 和 是同义词。datac_str
126赞 vitaut 8/22/2014 #16

C++ 20 具有 std::format,它在 API 方面类似于 API,但完全类型安全,适用于用户定义的类型,并使用类似 Python 的格式字符串语法。以下是格式化并将其写入流的方法:sprintfstd::string

std::cout << std::format("The answer is {}.", 42);

或者,您可以使用 {fmt} 库来格式化字符串并将其一次性写入文件流:stdout

fmt::print("The answer is {}.", 42);

至于这里的大多数其他答案,不幸的是,它们使用 varargs 并且本质上是不安全的,除非您使用像 GCC 属性这样的东西,它仅适用于文字格式字符串。您可以在以下示例中看到为什么这些函数不安全:sprintfformat

std::string format_str = "%s";
string_format(format_str, format_str[0]);

Erik Aronesty 的答案中的实现在哪里。此代码会编译,但当您尝试运行它时,它很可能会崩溃:string_format

$ g++ -Wall -Wextra -pedantic test.cc 
$ ./a.out 
Segmentation fault: 11

免责声明:我是 {fmt} 和 C++20 的作者。std::format

评论

0赞 Sérgio 2/20/2018
恕我直言,你错过了包括error: 'fmt' has not been declared
1赞 vitaut 2/20/2018
这只是一个片段,而不是完整的代码。显然,您需要包含 <fmt/format.h>并将代码放在函数中。
0赞 Sérgio 2/21/2018
对我来说不是那么明显,恕我直言,您应该将其包含在片段中,感谢您的反馈
0赞 Douglas Daseeco 9/22/2018
建立在埃里克·阿罗内斯蒂(Erik Aronesty)的答案之上的是一条红鲱鱼。他的第一个代码示例不安全,第二个代码示例效率低下且笨拙。如果任何 vprintf 系列函数的buf_siz为零,则不会写入任何内容,并且缓冲区可能是空指针,但仍会计算并返回返回值(将写入的字节数,不包括空终止符)。生产质量的答案在这里:stackoverflow.com/questions/2342162/......
1赞 Ciro Santilli OurBigBook.com 7/31/2019
类似的实现已添加到 C++20!stackoverflow.com/a/57286312/895245 FMT 目前声称支持它。干得真棒!fmt
11赞 Dacav 10/5/2014 #17

我对这个非常受欢迎的问题有两分钱。

引用类似 printf 函数的手册页

成功返回后,这些函数将返回打印的字符数(不包括用于结束字符串输出的空字节)。

函数 snprintf() 和 vsnprintf() 写入的字节数不超过 (包括终止的 null 字节 ('\0'))。如果输出由于此限制而被截断,则返回值是字符数(不包括终止的 null 字节),如果有足够的可用空间,该字符数将写入最终字符串。因此,大小或更大的返回值意味着输出被截断。

换句话说,一个健全的 C++11 实现应该是这样的:

#include <string>
#include <cstdio>

template <typename... Ts>
std::string fmt (const std::string &fmt, Ts... vs)
{
    char b;
    size_t required = std::snprintf(&b, 0, fmt.c_str(), vs...) + 1;
        // See comments: the +1 is necessary, while the first parameter
        //               can also be set to nullptr

    char bytes[required];
    std::snprintf(bytes, required, fmt.c_str(), vs...);

    return std::string(bytes);
}

它工作得很好:)

可变参数模板仅在 C++11 中受支持。Pixelpoint 的答案显示了使用旧编程风格的类似技术。

奇怪的是,C++没有这样的东西。他们最近添加了 to_string(),在我看来,这是向前迈出的一大步。我想知道他们是否会最终添加一个操作员.......formatstd::string

编辑

正如 alexk7 所指出的,在 的返回值上需要 A,因为我们需要为字节留出空间。直观地说,在大多数架构上,缺少 will 会导致整数被 .这将在计算 的实际参数 之后发生,因此效果应该不可见。+1std::snprintf\0+1required0requiredstd::snprintf

然而,这个问题可能会改变,例如在编译器优化中:如果编译器决定对变量使用寄存器怎么办?这种错误有时会导致安全问题。required

评论

1赞 alexk7 1/5/2015
snprintf 始终附加一个终止 null 字节,但返回没有它的字符数。这段代码不是总是跳过最后一个字符吗?
0赞 Dacav 1/5/2015
@alexk7,不错的收获!我正在更新答案。代码不会跳过最后一个字符,而是写入缓冲区末尾,可能在整数上(幸运的是,此时已经计算过了整数)。bytesrequired
1赞 iFreilicht 3/20/2015
只是一个小提示:如果缓冲区大小为 0,您可以传递 a 作为缓冲区参数,从而消除代码中的行。(来源nullptrchar b;)
0赞 Dacav 3/22/2015
@iFreilicht,修复了。也+1
2赞 Yannuth 3/17/2016
使用“char bytes[required]”将分配在堆栈而不是堆上,这在大幅面字符串上可能很危险。请考虑改用 use a new。扬恩
574赞 iFreilicht 10/7/2014 #18

现代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 的代码

请查看编译器支持页面,了解它是否在标准库实现中可用。

截至 2023 年 7 月 18 日,部分支持可从以下方式开始:

在所有其他情况下,您可以求助于下面的 C++11 解决方案,或使用与 具有相同语义的 {fmt}std::format


C++11

使用 C++11s 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 下获得许可。

逐行说明:

目的:使用 写入 a,然后将其转换为 .char*std::snprintfstd::string

首先,我们使用 中的特殊条件确定 char 数组的所需长度。从 cppreference.comsnprintf

返回值

[...]如果生成的字符串由于buf_size限制而被截断, 函数返回总字符数(不包括 终止 null-byte),如果限制是 不强加。

这意味着所需的大小是字符数加 1,以便 null 终止符将位于所有其他字符之后,并且可以再次被字符串构造函数切断。@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_sintsize_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 );

之后,我们当然可以将其用于预期用途,并将格式化的字符串写入 .snprintfchar[]

std::snprintf( buf.get(), size, format.c_str(), args ... );

最后,我们从中创建一个 new 并返回一个,确保省略末尾的 null 终止符。std::string

return std::string( buf.get(), buf.get() + size - 1 );

您可以在此处查看实际操作示例。


如果您还想在参数列表中使用,请查看此要点std::string


面向 Visual Studio 用户的其他信息:

此答案中所述,Microsoft 重命名为(是的,没有)。MS 进一步将其设置为已弃用,并建议改用 _snprintf_s,但不会接受缓冲区为零或小于格式化输出,如果发生这种情况,则不会计算输出长度。 因此,为了摆脱编译过程中的弃用警告,您可以在文件顶部插入以下行,其中包含以下用法:std::snprintf_snprintfstd::_snprintf_s_snprintf

#pragma warning(disable : 4996)

最后的思考

这个问题的很多答案都是在 C++11 之前编写的,并且使用固定的缓冲区长度或 varg。除非你坚持使用旧版本的C++,否则我不建议使用这些解决方案。理想情况下,采用 C++20 方式。

由于此答案中的 C++11 解决方案使用模板,因此如果大量使用,它可以生成相当多的代码。但是,除非您正在为二进制文件空间非常有限的环境进行开发,否则这不会成为问题,并且在清晰度和安全性方面仍比其他解决方案有了巨大的改进。

如果空间效率非常重要,那么这两个带有 vargs 和 vsnprintf 的解决方案可能会很有用。不要使用任何具有固定缓冲区长度的解决方案,那只会自找麻烦。

评论

3赞 iFreilicht 4/25/2015
@moooeeeep 原因有很多。首先,这里的目标是返回一个 std::string,而不是一个 c 字符串,所以你的意思可能是或类似的东西。其次,如果你要返回这样的 c 字符串,它会导致未定义的行为,因为保存你所指向的值的向量将在返回时失效。第三,当我开始学习C++时,该标准没有定义元素必须以什么顺序存储在 中,因此通过指针访问其存储是未定义的行为。现在它会起作用,但我认为这样做没有任何好处。return string(&buf[0], size);std::vector
7赞 Phil Williams 5/22/2015
我真的很喜欢这个解决方案,但是我认为该行应该是,否则你会得到一个末尾带有空字符的字符串。我发现 gcc 4.9 就是这种情况。return string(buf.get(), buf.get() + size);return string(buf.get(), buf.get() + size - 1);
4赞 Julien-L 7/21/2016
我的编译器抱怨这个解决方案(gcc 版本 5.4.0 20160609 Ubuntu 5.4.0-6ubuntu1~16.04.1):。这是有道理的,因为参数是一个变量,而不是文字,并且可能导致安全问题。更多信息请见:stackoverflow.com/q/9707569/143504stackoverflow.com/q/9306175/143504error: format not a string literal and no format arguments [-Werror=format-security]formatsnprintf()
4赞 Zitrax 2/1/2017
在 clang 3.9.1 中,将 std::string 传递给 %s 会导致编译错误(错误:无法通过变量函数传递非平凡类型的对象“std::__cxx11::basic_string<char>”;调用将在运行时中止 [-Wnon-pod-varargs]),但在 CL 19 中,它可以正常编译并在运行时崩溃。我可以打开任何警告标志以在 cl 的编译时咳嗽?
3赞 ManuelAtWork 6/7/2017
编译器将为参数类型的每个元组生成一个模板专用化。如果这个函数用不同的参数多次使用,那么就会生成和编译大量的代码。(代码膨胀)
14赞 aaa 10/11/2014 #19
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

评论

0赞 Darrin Cullop 8/14/2021
在所有答案中,这个答案有你想要的两件事:使用 C++ 类型安全 (no ),它直接写入 没有临时缓冲区。我发布了一个带有标题库的 Gist,它完全可以做到这一点,但我只会将其添加到这个答案中,因为它本质上是相同的想法(除了它也支持)。gist.github.com/dwcullop/aabb2007ba0dcd8c7e80a761545e6ca3va_liststd::stringstd::wstring
0赞 Darrin Cullop 8/14/2021
它还正确分配 null 终止符,但确保“\0”不包含在大小中,从而创建一个结果,该结果可以正常处理大小感知代码和预期 null 终止的代码。这是一个微妙但重要的细节,许多其他答案都忽略了。
1赞 ksridhar 12/14/2014 #20

这可以尝试一下。简单。不过,实际上没有使用字符串类的细微差别。

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

#include <string>
#include <exception>
using namespace std;

//---------------------------------------------------------------------

class StringFormatter
{
public:
    static string format(const char *format, ...);
};

string StringFormatter::format(const char *format, ...)
{
    va_list  argptr;

    va_start(argptr, format);

        char   *ptr;
        size_t  size;
        FILE   *fp_mem = open_memstream(&ptr, &size);
        assert(fp_mem);

        vfprintf (fp_mem, format, argptr);
        fclose (fp_mem);

    va_end(argptr);

    string ret = ptr;
    free(ptr);

    return ret;
}

//---------------------------------------------------------------------

int main(void)
{
    string temp = StringFormatter::format("my age is %d", 100);
    printf("%s\n", temp.c_str());

    return 0;
}
6赞 Thomas Perl 1/20/2015 #21

如果你使用的是 asprintf(3) 的系统, 你可以很容易地包装它:

#include <iostream>
#include <cstdarg>
#include <cstdio>

std::string format(const char *fmt, ...) __attribute__ ((format (printf, 1, 2)));

std::string format(const char *fmt, ...)
{
    std::string result;

    va_list ap;
    va_start(ap, fmt);

    char *tmp = 0;
    int res = vasprintf(&tmp, fmt, ap);
    va_end(ap);

    if (res != -1) {
        result = tmp;
        free(tmp);
    } else {
        // The vasprintf call failed, either do nothing and
        // fall through (will return empty string) or
        // throw an exception, if your code uses those
    }

    return result;
}

int main(int argc, char *argv[]) {
    std::string username = "you";
    std::cout << format("Hello %s! %d", username.c_str(), 123) << std::endl;
    return 0;
}

评论

2赞 Aaron McDaid 7/11/2015
我会在 之前添加此行作为声明,因为它告诉 gcc 检查参数的类型并使用 -Wall 给出一个体面的警告:formatstd::string format(const char *fmt, ...) __attribute__ ((format (printf, 1, 2)));
2赞 Aaron McDaid 7/11/2015
我刚刚添加了一个调用。“如果在调用 va_start 或 va_copy 返回的函数之前未调用 va_end,则行为未定义。- 文档va_end
1赞 Goblinhack 8/28/2017
您应该检查 vasprintf 的返回结果,因为失败时指针值未定义。因此,可能包括 <new> 并添加: if (size == -1) { throw std::bad_alloc();
0赞 Thomas Perl 8/28/2017
好点,我已经相应地修改了答案,我决定在那里放一个注释而不是做 ,因为我没有在我的代码库中使用 C++ 异常,对于这样做的人,他们可以轻松地根据源注释和您的注释添加它在这里。throw std::bad_alloc();
2赞 konsolebox 2/10/2015 #22

Dacavpixelpoint 的答案中汲取了这个想法。我玩了一会儿,得到了这个:

#include <cstdarg>
#include <cstdio>
#include <string>

std::string format(const char* fmt, ...)
{
    va_list vl;

    va_start(vl, fmt);
    int size = vsnprintf(0, 0, fmt, vl) + sizeof('\0');
    va_end(vl);

    char buffer[size];

    va_start(vl, fmt);
    size = vsnprintf(buffer, size, fmt, vl);
    va_end(vl);

    return std::string(buffer, size);
}

通过理智的编程实践,我相信代码应该足够了,但是我仍然对更安全的替代方案持开放态度,这些替代方案仍然足够简单并且不需要 C++11。


这是另一个版本,它利用初始缓冲区来防止在初始缓冲区已经足够时进行第二次调用。vsnprintf()

std::string format(const char* fmt, ...)
{

    va_list vl;
    int size;

    enum { INITIAL_BUFFER_SIZE = 512 };

    {
        char buffer[INITIAL_BUFFER_SIZE];

        va_start(vl, fmt);
        size = vsnprintf(buffer, INITIAL_BUFFER_SIZE, fmt, vl);
        va_end(vl);

        if (size < INITIAL_BUFFER_SIZE)
            return std::string(buffer, size);
    }

    size += sizeof('\0');

    char buffer[size];

    va_start(vl, fmt);
    size = vsnprintf(buffer, size, fmt, vl);
    va_end(vl);

    return std::string(buffer, size);
}

(原来这个版本和Piti Ongmongkolkul的答案差不多,只是没有使用和,还在创建时指定了大小。newdelete[]std::string

这里不使用 and 的想法意味着在堆上使用堆栈,因为它不需要调用分配和释放函数,但是如果使用不当,在某些(可能是旧的,或者可能只是易受攻击的)系统中缓冲溢出可能是危险的。如果这是一个问题,我强烈建议改用 AND。请注意,这里唯一关心的是已经使用限制调用的分配,因此根据第二个缓冲区上分配的大小指定限制也会阻止这些限制。newdelete[]newdelete[]vsnprintf()

20赞 user2622016 9/28/2015 #23

为了以“sprintf”方式格式化,请调用 (arguments and ) 以获取所需的缓冲区长度。使用 C++11 可变参数模板编写函数,如下所示:std::stringsnprintfnullptr0

#include <cstdio>
#include <string>
#include <cassert>

template< typename... Args >
std::string string_sprintf( const char* format, Args... args ) {
  int length = std::snprintf( nullptr, 0, format, args... );
  assert( length >= 0 );

  char* buf = new char[length + 1];
  std::snprintf( buf, length + 1, format, args... );

  std::string str( buf );
  delete[] buf;
  return str;
}

使用 C++11 支持进行编译,例如在 GCC 中:g++ -std=c++11

用法:

  std::cout << string_sprintf("%g, %g\n", 1.23, 0.001);

评论

0赞 Shital Shah 4/12/2016
std::snprintf 在 VC++12 (Visual Studio 2013) 中不可用。请改用 _snprintf 替换它。
0赞 Behrouz.M 5/19/2016
为什么不用代替?char buf[length + 1];char* buf = new char[length + 1];
1赞 user2622016 5/19/2016
using 和 with new 之间的区别在于,在前一种情况下,buf 将分配在堆栈上。对于小缓冲区来说是可以的,但由于我们无法保证结果字符串的大小,因此使用 .例如,在我的机器上,在数字 5 之前打印数量惊人的前导零,在堆栈上使用数组时核心转储,但在使用动态分配的数组时工作正常char[]char*newstring_sprintf("value: %020000000d",5)new char[length + 1]
0赞 Eddie Deng 9/3/2016
非常聪明的主意,可以获得格式化输出所需的实际 buff 大小
1赞 Mihai Todor 6/18/2019
@user2622016:谢谢你的解决方案!请注意,这是多余的std::move
2赞 Shital Shah 5/29/2016 #24

到目前为止,这里的所有答案似乎都存在以下一个或多个问题:(1)它可能不适用于VC++(2)它需要额外的依赖项,如boost或fmt(3)它过于复杂的自定义实现,可能没有经过很好的测试。

下面的代码解决了上述所有问题。

#include <string>
#include <cstdarg>
#include <memory>

std::string stringf(const char* format, ...)
{
    va_list args;
    va_start(args, format);
    #ifndef _MSC_VER
        
        //GCC generates warning for valid use of snprintf to get
        //size of result string. We suppress warning with below macro.
        #ifdef __GNUC__
        #pragma GCC diagnostic push
        #pragma GCC diagnostic ignored "-Wformat-nonliteral"
        #endif
        
        size_t size = std::snprintf(nullptr, 0, format, args) + 1; // Extra space for '\0'

        #ifdef __GNUC__
        # pragma GCC diagnostic pop
        #endif
        
        std::unique_ptr<char[]> buf(new char[ size ] ); 
        std::vsnprintf(buf.get(), size, format, args);
        return std::string(buf.get(), buf.get() + size - 1 ); // We don't want the '\0' inside
    #else
        int size = _vscprintf(format, args);
        std::string result(size, 0);
        vsnprintf_s((char*)result.data(), size + 1, _TRUNCATE, format, args);
        return result;
    #endif
    va_end(args);
}    

int main() {
    float f = 3.f;
    int i = 5;
    std::string s = "hello!";
    auto rs = stringf("i=%d, f=%f, s=%s", i, f, s.c_str());
    printf("%s", rs.c_str());
    return 0;
}

笔记:

  1. 单独的 VC++ 代码分支是必要的,因为 VC++ 已决定弃用该分支,这将为上述其他高投票答案生成编译器警告。由于我总是在“警告作为错误”模式下运行,因此它不适合我。snprintf
  2. 该函数接受而不是 。这是因为大多数时候这个函数会用文字字符串来调用,这确实是 ,而不是 。如果您确实有 format 参数,则只需调用 .char *std::stringchar *std::stringstd::string.c_str()
  3. 该函数的名称是 stringf,而不是像 string_format to keep up with printf, scanf 等。
  4. 它没有解决安全问题(即错误的参数可能会导致 seg 错误而不是异常)。如果你需要这个,那么你最好使用boost或fmt库。我在这里的偏好是 fmt,因为它只是一个要放入项目的标题和源文件,同时具有比 boost 更奇怪的格式语法。但是,两者都与 printf 格式字符串不兼容,因此在这种情况下,下面仍然有用。
  5. stringf 代码通过 GCC 严格模式编译。这需要额外的宏来抑制 GCC 警告中的误报。#pragma

上面的代码已经过测试,

评论

0赞 Dino Dini 2/11/2023
极好的寂寞答案。始终自下而上查看 StackOverflow 页面!
0赞 Dino Dini 2/11/2023
std::string result(size, 0);vsnprintf_s((char*)result.data(), size+1, _TRUNCATE, format, args);如果出现以下情况,则防止向生成的字符串添加额外的 null 字符_MSC_VER
3赞 Folkert van Heusden 6/23/2016 #25

我通常用这个:

std::string myformat(const char *const fmt, ...)
{
        char *buffer = NULL;
        va_list ap;

        va_start(ap, fmt);
        (void)vasprintf(&buffer, fmt, ap);
        va_end(ap);

        std::string result = buffer;
        free(buffer);

        return result;
}

缺点:并非所有系统都支持 vasprint

评论

0赞 Goblinhack 8/28/2017
Vasprintf 很好 - 但是您需要检查返回代码。On -1 缓冲区将具有未定义的值。需要: if (size == -1) { throw std::bad_alloc();
2赞 Pawel Sledzikowski 10/11/2016 #26

下面略微修改了@iFreilicht答案的版本,更新为 C++14(使用函数而不是原始声明)并添加了对参数的支持(基于 Kenny Kerr 的文章make_uniquestd::string)

#include <iostream>
#include <memory>
#include <string>
#include <cstdio>

template <typename T>
T process_arg(T value) noexcept
{
    return value;
}

template <typename T>
T const * process_arg(std::basic_string<T> const & value) noexcept
{
    return value.c_str();
}

template<typename ... Args>
std::string string_format(const std::string& format, Args const & ... args)
{
    const auto fmt = format.c_str();
    const size_t size = std::snprintf(nullptr, 0, fmt, process_arg(args) ...) + 1;
    auto buf = std::make_unique<char[]>(size);
    std::snprintf(buf.get(), size, fmt, process_arg(args) ...);
    auto res = std::string(buf.get(), buf.get() + size - 1);
    return res;
}

int main()
{
    int i = 3;
    float f = 5.f;
    char* s0 = "hello";
    std::string s1 = "world";
    std::cout << string_format("i=%d, f=%f, s=%s %s", i, f, s0, s1) << "\n";
}

输出:

i = 3, f = 5.000000, s = hello world

如果需要,请随时将此答案与原始答案合并。

25赞 Douglas Daseeco 4/13/2018 #27

经过测试,生产质量答案

此答案使用符合标准的技术处理一般情况。在页面底部附近的 CppReference.com 上给出了相同的方法作为示例。与示例不同的是,此代码符合问题的要求,并在机器人和卫星应用中进行了现场测试。它还改进了评论。设计质量将在下文中进一步讨论。

#include <string>
#include <cstdarg>
#include <vector>

// requires at least C++11
const std::string vformat(const char * const zcFormat, ...) {

    // initialize use of the variable argument array
    va_list vaArgs;
    va_start(vaArgs, zcFormat);

    // reliably acquire the size
    // from a copy of the variable argument array
    // and a functionally reliable call to mock the formatting
    va_list vaArgsCopy;
    va_copy(vaArgsCopy, vaArgs);
    const int iLen = std::vsnprintf(NULL, 0, zcFormat, vaArgsCopy);
    va_end(vaArgsCopy);

    // return a formatted string without risking memory mismanagement
    // and without assuming any compiler or platform specific behavior
    std::vector<char> zc(iLen + 1);
    std::vsnprintf(zc.data(), zc.size(), zcFormat, vaArgs);
    va_end(vaArgs);
    return std::string(zc.data(), iLen); }

#include <ctime>
#include <iostream>
#include <iomanip>

// demonstration of use
int main() {

    std::time_t t = std::time(nullptr);
    std::cerr
        << std::put_time(std::localtime(& t), "%D %T")
        << " [debug]: "
        << vformat("Int 1 is %d, Int 2 is %d, Int 3 is %d", 11, 22, 33)
        << std::endl;
    return 0; }

可预测的线性效率

根据问题规范,两次通过是安全、可靠和可预测的可重用功能的必要条件。关于可重用函数中 varg 大小分布的假设是糟糕的编程风格,应避免。在这种情况下,任意大的变量长度表示是选择算法的关键因素。

溢出时重试效率呈指数级低,这是 C++11 标准委员会讨论上述建议时讨论的另一个原因,即在写入缓冲区为 null 时提供试运行。

在上面的生产就绪实现中,第一次运行就是这样的试运行,以确定分配大小。不进行分配。几十年来,printf 指令的解析和 vargs 的读取已经变得非常高效。可重用的代码应该是可预测的,即使必须牺牲微不足道的低效率。

安全可靠

安德鲁·柯尼希(Andrew Koenig)在剑桥的一次活动上发表演讲后对我们中的一小群人说:“用户功能不应该依赖于利用故障来获得非异常功能。像往常一样,他的智慧在记录中得到了证明。已修复和已关闭的安全 bug 问题通常表明在修复之前利用的漏洞的描述中存在重试黑客攻击。

这在 sprintf 的替代方案、C9X 修订提案、ISO IEC 文档 WG14 N645/X3J11 96-008 中对空缓冲区功能的正式标准修订提案中有所提及。在动态内存可用性的约束下,每个打印指令插入的任意长字符串“%s”不是例外,不应被利用它来产生“异常功能”。

考虑该提案以及本答案第一段中链接到的 C++Reference.org 页面底部给出的示例代码。

此外,失败案例的测试很少像成功案例那样强大。

可移植性

所有主要的 OS 供应商都提供完全支持 std::vsnprintf 的编译器,作为 c++11 标准的一部分。出于多种原因,运行不再维护发行版的供应商的产品的主机应配备 g++ 或 clang++。

堆栈使用

对 std::vsnprintf 的第一次调用中的堆栈使用量将小于或等于第二次调用,并且将在第二次调用开始之前释放。如果第一次调用超过堆栈可用性,则 std::fprintf 也会失败。

评论

0赞 Engineerist 12/22/2018
简短而稳健。它可能会在具有不合格 vsnprintf-s 的 HP-UX、IRIX、Tru64 上失败。编辑:另外,考虑到两次传递如何影响性能,特别是对于最常见的小字符串格式,您是否考虑过对初始传递的猜测,这可能足够大?
2赞 Engineerist 12/23/2018
FWIW,我所指的猜测使用堆栈分配的缓冲区,其中发生了第一次运行。如果合适,则可以节省第二次运行的成本以及在那里发生的动态分配。据推测,小字符串比大字符串更频繁地使用。在我的粗略基准测试中,该策略(几乎)将小字符串的运行时间减半,并且与上述策略相差几个百分点(也许是固定开销?您能否详细说明采用试运行等的 C++11 设计?我想阅读它。
0赞 Douglas Daseeco 12/24/2018
@Engineerist,您的问题已在代码上方和下方的答案正文中得到解决。这样可以使子主题更容易阅读。
2赞 Jerry Jeremiah 10/9/2020
那么为什么你用 a 而不是 a 呢?std::vector<char>std::string
2赞 CasseroleBoi 4/16/2023
这些括号是这里真正的罪行
2赞 Andry 10/12/2018 #28

更新 1:添加了测试fmt::format

我围绕这里介绍的方法进行了自己的调查,并获得了与这里提到的截然相反的结果。

我在 4 种方法中使用了 4 个函数:

  • 变调功能 +vsnprintf + std::unique_ptr
  • 变调功能 +vsnprintf + std::string
  • 可变参数模板函数 +std::ostringstream + std::tuple + utility::for_each
  • fmt::format库中的函数fmt

对于已使用的测试后端。googletest

#include <string>
#include <cstdarg>
#include <cstdlib>
#include <memory>
#include <algorithm>

#include <fmt/format.h>

inline std::string string_format(size_t string_reserve, const std::string fmt_str, ...)
{
    size_t str_len = (std::max)(fmt_str.size(), string_reserve);

    // plain buffer is a bit faster here than std::string::reserve
    std::unique_ptr<char[]> formatted;

    va_list ap;
    va_start(ap, fmt_str);

    while (true) {
        formatted.reset(new char[str_len]);

        const int final_n = vsnprintf(&formatted[0], str_len, fmt_str.c_str(), ap);

        if (final_n < 0 || final_n >= int(str_len))
            str_len += (std::abs)(final_n - int(str_len) + 1);
        else
            break;
    }

    va_end(ap);

    return std::string(formatted.get());
}

inline std::string string_format2(size_t string_reserve, const std::string fmt_str, ...)
{
    size_t str_len = (std::max)(fmt_str.size(), string_reserve);
    std::string str;

    va_list ap;
    va_start(ap, fmt_str);

    while (true) {
        str.resize(str_len);

        const int final_n = vsnprintf(const_cast<char *>(str.data()), str_len, fmt_str.c_str(), ap);

        if (final_n < 0 || final_n >= int(str_len))
            str_len += (std::abs)(final_n - int(str_len) + 1);
        else {
            str.resize(final_n); // do not forget to shrink the size!
            break;
        }
    }

    va_end(ap);

    return str;
}

template <typename... Args>
inline std::string string_format3(size_t string_reserve, Args... args)
{
    std::ostringstream ss;
    if (string_reserve) {
        ss.rdbuf()->str().reserve(string_reserve);
    }
    std::tuple<Args...> t{ args... };
    utility::for_each(t, [&ss](auto & v)
    {
        ss << v;
    });
    return ss.str();
}

实现取自此处:迭代元组for_each

#include <type_traits>
#include <tuple>

namespace utility {

    template <std::size_t I = 0, typename FuncT, typename... Tp>
    inline typename std::enable_if<I == sizeof...(Tp), void>::type
        for_each(std::tuple<Tp...> &, const FuncT &)
    {
    }

    template<std::size_t I = 0, typename FuncT, typename... Tp>
    inline typename std::enable_if<I < sizeof...(Tp), void>::type
        for_each(std::tuple<Tp...> & t, const FuncT & f)
    {
        f(std::get<I>(t));
        for_each<I + 1, FuncT, Tp...>(t, f);
    }

}

测试:

TEST(ExternalFuncs, test_string_format_on_unique_ptr_0)
{
    for (size_t i = 0; i < 1000000; i++) {
        const std::string v = string_format(0, "%s+%u\n", "test test test", 12345);
        UTILITY_SUPPRESS_OPTIMIZATION_ON_VAR(v);
    }
}

TEST(ExternalFuncs, test_string_format_on_unique_ptr_256)
{
    for (size_t i = 0; i < 1000000; i++) {
        const std::string v = string_format(256, "%s+%u\n", "test test test", 12345);
        UTILITY_SUPPRESS_OPTIMIZATION_ON_VAR(v);
    }
}

TEST(ExternalFuncs, test_string_format_on_std_string_0)
{
    for (size_t i = 0; i < 1000000; i++) {
        const std::string v = string_format2(0, "%s+%u\n", "test test test", 12345);
        UTILITY_SUPPRESS_OPTIMIZATION_ON_VAR(v);
    }
}

TEST(ExternalFuncs, test_string_format_on_std_string_256)
{
    for (size_t i = 0; i < 1000000; i++) {
        const std::string v = string_format2(256, "%s+%u\n", "test test test", 12345);
        UTILITY_SUPPRESS_OPTIMIZATION_ON_VAR(v);
    }
}

TEST(ExternalFuncs, test_string_format_on_string_stream_on_variadic_tuple_0)
{
    for (size_t i = 0; i < 1000000; i++) {
        const std::string v = string_format3(0, "test test test", "+", 12345, "\n");
        UTILITY_SUPPRESS_OPTIMIZATION_ON_VAR(v);
    }
}

TEST(ExternalFuncs, test_string_format_on_string_stream_on_variadic_tuple_256)
{
    for (size_t i = 0; i < 1000000; i++) {
        const std::string v = string_format3(256, "test test test", "+", 12345, "\n");
        UTILITY_SUPPRESS_OPTIMIZATION_ON_VAR(v);
    }
}

TEST(ExternalFuncs, test_string_format_on_string_stream_inline_0)
{
    for (size_t i = 0; i < 1000000; i++) {
        std::ostringstream ss;
        ss << "test test test" << "+" << 12345 << "\n";
        const std::string v = ss.str();
        UTILITY_SUPPRESS_OPTIMIZATION_ON_VAR(v);
    }
}

TEST(ExternalFuncs, test_string_format_on_string_stream_inline_256)
{
    for (size_t i = 0; i < 1000000; i++) {
        std::ostringstream ss;
        ss.rdbuf()->str().reserve(256);
        ss << "test test test" << "+" << 12345 << "\n";
        const std::string v = ss.str();
        UTILITY_SUPPRESS_OPTIMIZATION_ON_VAR(v);
    }
}

TEST(ExternalFuncs, test_fmt_format_positional)
{
    for (size_t i = 0; i < 1000000; i++) {
        const std::string v = fmt::format("{0:s}+{1:d}\n", "test test test", 12345);
        UTILITY_SUPPRESS_OPTIMIZATION_ON_VAR(v);
    }
}

TEST(ExternalFuncs, test_fmt_format_named)
{
    for (size_t i = 0; i < 1000000; i++) {
        const std::string v = fmt::format("{first:s}+{second:d}\n", fmt::arg("first", "test test test"), fmt::arg("second", 12345));
        UTILITY_SUPPRESS_OPTIMIZATION_ON_VAR(v);
    }
}

这。UTILITY_SUPPRESS_OPTIMIZATION_ON_VAR

未起诉.hpp

#define UTILITY_SUPPRESS_OPTIMIZATION_ON_VAR(var)   ::utility::unused_param(&var)

namespace utility {

    extern const volatile void * volatile g_unused_param_storage_ptr;

    extern void
#ifdef __GNUC__
    __attribute__((optimize("O0")))
#endif
        unused_param(const volatile void * p);

}

未使用的 .cpp

namespace utility {

    const volatile void * volatile g_unused_param_storage_ptr = nullptr;

    void
#ifdef __GNUC__
    __attribute__((optimize("O0")))
#endif
        unused_param(const volatile void * p)
    {
        g_unused_param_storage_ptr = p;
    }

}

结果

[ RUN      ] ExternalFuncs.test_string_format_on_unique_ptr_0
[       OK ] ExternalFuncs.test_string_format_on_unique_ptr_0 (556 ms)
[ RUN      ] ExternalFuncs.test_string_format_on_unique_ptr_256
[       OK ] ExternalFuncs.test_string_format_on_unique_ptr_256 (331 ms)
[ RUN      ] ExternalFuncs.test_string_format_on_std_string_0
[       OK ] ExternalFuncs.test_string_format_on_std_string_0 (457 ms)
[ RUN      ] ExternalFuncs.test_string_format_on_std_string_256
[       OK ] ExternalFuncs.test_string_format_on_std_string_256 (279 ms)
[ RUN      ] ExternalFuncs.test_string_format_on_string_stream_on_variadic_tuple_0
[       OK ] ExternalFuncs.test_string_format_on_string_stream_on_variadic_tuple_0 (1214 ms)
[ RUN      ] ExternalFuncs.test_string_format_on_string_stream_on_variadic_tuple_256
[       OK ] ExternalFuncs.test_string_format_on_string_stream_on_variadic_tuple_256 (1325 ms)
[ RUN      ] ExternalFuncs.test_string_format_on_string_stream_inline_0
[       OK ] ExternalFuncs.test_string_format_on_string_stream_inline_0 (1208 ms)
[ RUN      ] ExternalFuncs.test_string_format_on_string_stream_inline_256
[       OK ] ExternalFuncs.test_string_format_on_string_stream_inline_256 (1302 ms)
[ RUN      ] ExternalFuncs.test_fmt_format_positional
[       OK ] ExternalFuncs.test_fmt_format_positional (288 ms)
[ RUN      ] ExternalFuncs.test_fmt_format_named
[       OK ] ExternalFuncs.test_fmt_format_named (392 ms)

正如你所看到的,通过 + 实现等于 ,但比通过 + 更快,而 + 比通过 .vsnprintfstd::stringfmt::formatvsnprintfstd::unique_ptrstd::ostringstream

在 中编译并运行的测试。Visual Studio 2015 Update 3Windows 7 x64 / Intel Core i7-4820K CPU @ 3.70GHz / 16GB

20赞 Ciro Santilli OurBigBook.com 7/31/2019 #29

C++20 std::格式

它来了!该功能在以下位置进行了描述:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0645r9.html 并使用类似 Python 的语法。.format()

我希望用法是这样的:

#include <format>
#include <string>

int main() {
    std::string message = std::format("The answer is {}.", 42);
}

GCC 9.1.0 仍然不支持它。g++-9 -std=c++2a

现有库在获得官方支持之前实现它: https://github.com/fmtlib/fmt 如前所述: std::string 格式化,如 sprintf 在 Ubuntu 上安装 22.04:fmt

sudo apt install libfmt-dev

修改源以替换:

  • <format><fmt/core.h>
  • std::formatfmt::format

主 .cpp

#include <string>
#include <iostream>

#include <fmt/core.h>

int main() {
    std::string message = fmt::format("The answer is {}.", 42);
    std::cout << message << std::endl;
}

并使用以下命令进行编译和运行:

g++ -std=c++11 -o main.out main.cpp -lfmt
./main.out

输出:

The answer is 42.

API 将添加一个新的标头:std::format

建议的格式设置 API 在新标头中定义,应该不会对现有代码产生影响。<format>

十六进制格式 {:x}

C++ cout 十六进制值?

前导零 {:03}

使用 C++ 输出运算符打印前导零?

左对齐 {:<},右对齐 {:>},居中 {:^}

打印 cout <<时C++对齐

浮点精度 {:.2}

显示正数上的符号 {:+}

如何在 C++ 中打印带有前缀 + 的正数

将布尔值显示为 truefalse{:}

在 C++ 中将布尔转换为文本

2赞 Amit 8/1/2022 #30

(这在 VC++ VS2019、VS2022 上运行良好)

C++ 17 解决方案(这将适用于 std::string 和 std::wstring):

分配一个缓冲区,格式化到其中,然后将其复制到另一个字符串中是无效的。可以创建格式化字符串大小的 std::string 并直接格式化到该字符串缓冲区中:

#include <string>
#include <stdexcept>
#include <cwchar>
#include <cstdio>
#include <type_traits>

template<typename T, typename ... Args>
std::basic_string<T> string_format(T const* const format, Args ... args)
{
    int size_signed{ 0 };

    // 1) Determine size with error handling:    
    if constexpr (std::is_same_v<T, char>) { // C++17
        size_signed = std::snprintf(nullptr, 0, format, args ...);
    }
    else {
        size_signed = std::swprintf(nullptr, 0, format, args ...);
    }  
    if (size_signed <= 0) {
        throw std::runtime_error("error during formatting.");
    }
    const auto size = static_cast<size_t>(size_signed);

    // 2) Prepare formatted string:
    std::basic_string<T> formatted(size, T{});
    if constexpr (std::is_same_v<T, char>) { // C++17
        std::snprintf(formatted.data(), size + 1, format, args ...); // +1 for the '\0' (it will not be part of formatted).
    }
    else {
        std::swprintf(formatted.data(), size + 1, format, args ...); // +1 for the '\0' (it will not be part of formatted).
    }

    return formatted; // Named Return Value Optimization (NRVO), avoids an unnecessary copy. 
}

另外:通常,format 参数是 char[] / wchar_t[] & 创建 std::string 对象效率不高。传递 char* 或 wchar_t* & 如果你已经有一个 std::string 对象,你仍然可以将其用作 your_string.c_str()。例:

int main()
{
    int i{ 0 };

    // The format parameter is a char[] / wchar_t[]:

    const std::string title1 = string_format("story[%d].", ++i); // => "story[1]"

    const std::wstring title2 = string_format(L"story[%d].", ++i); // => L"story[2]"

    // If you already have a std::string object:

    const std::string format1{ "story[%d]." };
    const std::string title3 = string_format(format1.c_str(), ++i); // => "story[3]"

    const std::wstring format2{ L"story[%d]." };
    const std::wstring title4 = string_format(format2.c_str(), ++i); // => L"story[4]"  
}

评论

0赞 Barracudach 9/3/2022
为什么写“T const* const format”,而不是“const T* const format”?
0赞 Amit 9/3/2022
@Barracudach 规则:“const 适用于它剩下的东西。如果左边什么都没有,那么它适用于右边的东西。