【问题标题】:C++: how to get fprintf results as a std::string w/o sprintfC++:如何将 fprintf 结果作为 std::string 不带 sprintf
【发布时间】:2008-09-16 06:12:48
【问题描述】:

我正在使用一个用 C++ 实现的开源 UNIX 工具,我需要更改一些代码来让它做我想做的事情。我想做最小的改变,希望我的补丁在上游被接受。可在标准 C++ 中实现且不会创建更多外部依赖项的解决方案是首选。

这是我的问题。我有一个 C++ 类——我们称之为“A”——它目前使用 fprintf() 将其高度格式化的数据结构打印到文件指针。在它的打印函数中,它还递归地调用了几个成员类的相同定义的打印函数(“B”就是一个例子)。还有另一个类 C 有一个成员 std::string "foo",需要将其设置为 A 实例的 print() 结果。将其视为 A 的 to_str() 成员函数。

在伪代码中:

class A {
public:
  ...

  void print(FILE* f);
  B b;

  ...  
};

...

void A::print(FILE *f)
{
  std::string s = "stuff";
  fprintf(f, "some %s", s);
  b.print(f);
}

class C {
  ...
  std::string foo;
  bool set_foo(std::str);
  ...
}

...

A a = new A();
C c = new C();

...

// wish i knew how to write A's to_str()
c.set_foo(a.to_str());

我应该提到 C 相当稳定,但 A 和 B(以及 A 的其他依赖项)处于不断变化的状态,因此所需的代码更改越少越好。当前的 print(FILE* F) 接口也需要保留。我考虑了几种实现 A::to_str() 的方法,每种方法都有优点和缺点:

  1. 将对 fprintf() 的调用更改为 sprintf()

    • 我不必重写任何格式字符串
    • print() 可以重新实现为:fprint(f, this.to_str());
    • 但我需要手动分配char[]s,合并很多c字符串,最后将字符数组转换为std::string
  2. 尝试在字符串流中捕获 a.print() 的结果

    • 我必须将所有格式字符串转换为
    • print() 必须重写,因为据我所知没有标准方法可以从 UNIX 文件句柄创建输出流(尽管 this guy says it may be possible)。
  3. 使用Boost的字符串format library

    • 更多的外部依赖。糟糕。
    • Format 的语法与 printf() 的语法大相径庭,令人讨厌:

    printf(format_str, args) -> cout

  4. 使用Qt的QString::asprintf()

    • 不同的外部依赖项。

那么,我是否已经用尽了所有可能的选择?如果是这样,你认为哪个是我最好的选择?如果没有,我忽略了什么?

谢谢。

【问题讨论】:

  • 虽然我已经回答了这个问题,但我也想指出这个项目:github.com/c42f/tinyformat,它很好地解决了这个问题,并且在再现 printf 格式符号方面确实做得很好。这些天来,我直接使用该软件包,而不是几年前我在其他答案中详述的 vsprintf 方法。

标签: c++ unix format printf stdstring


【解决方案1】:

这是我喜欢的用于使功能与 'sprintf' 相同但返回 std::string 并且不受缓冲区溢出问题影响的习语。这段代码是我正在编写的一个开源项目(BSD 许可)的一部分,所以每个人都可以随意使用它。

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

std::string
format (const char *fmt, ...)
{
    va_list ap;
    va_start (ap, fmt);
    std::string buf = vformat (fmt, ap);
    va_end (ap);
    return buf;
}



std::string
vformat (const char *fmt, va_list ap)
{
    // Allocate a buffer on the stack that's big enough for us almost
    // all the time.
    size_t size = 1024;
    char buf[size];

    // Try to vsnprintf into our buffer.
    va_list apcopy;
    va_copy (apcopy, ap);
    int needed = vsnprintf (&buf[0], size, fmt, ap);
    // NB. On Windows, vsnprintf returns -1 if the string didn't fit the
    // buffer.  On Linux & OSX, it returns the length it would have needed.

    if (needed <= size && needed >= 0) {
        // It fit fine the first time, we're done.
        return std::string (&buf[0]);
    } else {
        // vsnprintf reported that it wanted to write more characters
        // than we allotted.  So do a malloc of the right size and try again.
        // This doesn't happen very often if we chose our initial size
        // well.
        std::vector <char> buf;
        size = needed;
        buf.resize (size);
        needed = vsnprintf (&buf[0], size, fmt, apcopy);
        return std::string (&buf[0]);
    }
}

编辑:当我编写此代码时,我不知道这需要 C99 一致性,并且 Windows(以及较旧的 glibc)具有不同的 vsnprintf 行为,其中它返回 -1 表示失败,而不是确定的度量需要多少空间。这是我修改后的代码,大家可以看一下,如果你觉得没问题,我会再次编辑,使其成为列出的唯一成本:

std::string
Strutil::vformat (const char *fmt, va_list ap)
{
    // Allocate a buffer on the stack that's big enough for us almost
    // all the time.  Be prepared to allocate dynamically if it doesn't fit.
    size_t size = 1024;
    char stackbuf[1024];
    std::vector<char> dynamicbuf;
    char *buf = &stackbuf[0];
    va_list ap_copy;

    while (1) {
        // Try to vsnprintf into our buffer.
        va_copy(ap_copy, ap);
        int needed = vsnprintf (buf, size, fmt, ap);
        va_end(ap_copy);

        // NB. C99 (which modern Linux and OS X follow) says vsnprintf
        // failure returns the length it would have needed.  But older
        // glibc and current Windows return -1 for failure, i.e., not
        // telling us how much was needed.

        if (needed <= (int)size && needed >= 0) {
            // It fit fine so we're done.
            return std::string (buf, (size_t) needed);
        }

        // vsnprintf reported that it wanted to write more characters
        // than we allotted.  So try again using a dynamic buffer.  This
        // doesn't happen very often if we chose our initial size well.
        size = (needed > 0) ? (needed+1) : (size*2);
        dynamicbuf.resize (size);
        buf = &dynamicbuf[0];
    }
}

【讨论】:

  • 在格式/vformat 方面做得很好。也许stackoverflow需要某种代码片段共享部分:-)
  • 如果结果字符串大于 1024 字节,您的代码似乎将无法工作。根据 MSDN:vsnprintf - Return Value ... 如果要写入的字符数大于 count,这些函数返回 -1 表示输出已被截断。
  • 如果您不打算在循环中执行此操作,为什么要使用可变数组大小?无需复制“ap”,这可能很昂贵。 GNU [v]s[n]printf 手册页有一个如何更便携地执行此操作的示例:tin.org/bin/man.cgi?section=3&topic=snprintf
  • 另外,在创建字符串时使用来自 vsnprintf 的返回值。这将为您节省一个额外的 strlen 调用。
  • @kevinarpe 它已经存在很多年了。但我已经转载了上面的相关部分。但现在,我强烈建议使用完全类型安全的 C++ 版本,例如 tinyformat 或其他类似项目。
【解决方案2】:

我正在使用 #3:boost 字符串格式库 - 但我必须承认,我从未对格式规范的差异有任何问题。

对我来说就像一个魅力 - 外部依赖可能会更糟(一个非常稳定的库)

已编辑:添加一个如何使用 boost::format 代替 printf 的示例:

sprintf(buffer, "This is a string with some %s and %d numbers", "strings", 42);

在 boost::format 库中会是这样的:

string = boost::str(boost::format("This is a string with some %s and %d numbers") %"strings" %42);

希望这有助于澄清 boost::format 的用法

我在 4 或 5 个应用程序中使用 boost::format 作为 sprintf / printf 替换(将格式化的字符串写入文件,或自定义输出到日志文件)并且从未遇到格式差异问题。可能有一些(或多或少晦涩)不同的格式说明符 - 但我从来没有遇到过问题。

相比之下,我有一些格式规范,我无法真正使用流(据我记得)

【讨论】:

  • 感谢您对 boost::format 用法的澄清。考虑到这个项目已经依赖于另一个 boost 库,这很诱人,但我认为没有什么能像 Loki 那样与 std::string 一起工作的 printf 更好。
  • 我尝试了 Loki 的 SafeFormat,但事实证明它只是将 boost 的 5s 换成了 ()s。积极的一面是,一旦我采用了 boost::format,我的代码就可以工作了 :-)
  • 很高兴听到 boost::format 对你有用 - 从未尝试过 Loki 方法。
【解决方案3】:

您可以使用带格式的 std::string 和 iostreams,例如 setw() 调用和 iomanip 中的其他调用

【讨论】:

  • 感谢您让我了解 iomanip。它看起来确实很有用,但我想尽量保留大量现有的格式字符串。
  • 抱歉,iostream 及其操纵器是 printf() 格式字符串的一个非常糟糕的替代品。
【解决方案4】:

以下可能是替代解决方案:

void A::printto(ostream outputstream) {
    char buffer[100];
    string s = "stuff";
    sprintf(buffer, "some %s", s);
    outputstream << buffer << endl;
    b.printto(outputstream);
}

B::printto类似),并定义

void A::print(FILE *f) {
    printto(ofstream(f));
}

string A::to_str() {
    ostringstream os;
    printto(os);
    return os.str();
}

当然,您应该真正使用 snprintf 而不是 sprintf 以避免缓冲区溢出。您还可以有选择地将风险更大的 sprintfs 更改为

【讨论】:

  • 我会将您的回复视为#1 和#2 的组合 :-) ofstream 是否有一个接受文件句柄的构造函数?我的印象是他们不兼容......
【解决方案5】:

您应该尝试 Loki 库的 SafeFormat 头文件 (http://loki-lib.sourceforge.net/index.php?n=Idioms.Printf)。它类似于 boost 的字符串格式库,但保留了 printf(...) 函数的语法。

我希望这会有所帮助!

【讨论】:

【解决方案6】:

The {fmt} library 提供了fmt::sprintf 函数,该函数执行printf 兼容格式(包括根据POSIX specification 的位置参数)并将结果返回为std::string

std::string s = fmt::sprintf("The answer is %d.", 42);

免责声明:我是这个库的作者。

【讨论】:

    【解决方案7】:

    这是关于序列化的吗?还是正确打印? 如果是前者,也要考虑 boost::serialization。这都是关于对象和子对象的“递归”序列化。

    【讨论】:

    • 这是关于正确打印的。 C.foo 是最终(大部分)按原样显示给用户的一条数据。如果这是我的代码,我会失去 print(FILE*) 的废话,这太严格了。
    猜你喜欢
    • 1970-01-01
    • 2014-03-29
    • 2016-08-22
    • 2016-10-30
    • 1970-01-01
    • 1970-01-01
    • 2013-09-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多