【问题标题】:How to use C++ std::ostream with printf-like formatting?如何使用具有类似 printf 格式的 C++ std::ostream?
【发布时间】:2013-02-27 07:07:00
【问题描述】:

我正在学习 C++。 coutstd::ostream 类的一个实例。 如何用它打印格式化的字符串?

我仍然可以使用printf,但我想学习一种可以利用所有 C++ 优势的正确 C++ 方法。我认为std::ostream 应该可以做到这一点,但我找不到正确的方法。

【问题讨论】:

  • 此页面不是流格式的综合指南Output Formatting
  • 你不应该真的需要它,因为你可以做类似cout << "my baby's age is " << 3 << endl;而不是printf("My baby's age is %u\n", 3);的事情
  • cout << "my baby's age is " << 3 << endl; 不可本地化;在非英语语言中,您可能有不同的单词顺序。因此,这种语法对于多语言应用程序是不可接受的。强制这种语法工作的唯一方法是使 switch 取决于语言 ID,这样的解决方案很丑陋。 printf 更好,因为翻译器可以翻译整个格式字符串并更改单词顺序,而无需修改每种不常见语言的源代码。

标签: c++ formatting printf ostream


【解决方案1】:

您可以直接使用std::ostream 做的唯一事情是众所周知的<<-syntax:

int i = 0;
std::cout << "this is a number: " << i;

还有各种IO manipulators可以用来影响整数、浮点数等的格式、位数等。

但是,这与printf 的格式化字符串不同。 C++11 不包括任何允许您以与printf 相同的方式使用字符串格式的工具(printf 本身除外,如果您愿意,当然可以在 C++ 中使用它)。

就提供printf-style 功能的库而言,有boost::format,它启用了这样的代码(从概要复制):

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

另请注意,在标准的未来版本中存在printf 样式的proposal for inclusion 格式。如果这被接受,可能会出现如下语法:

std::cout << std::putf("this is a number: %d\n",i);

【讨论】:

  • 字符串生成怎么样?也没有办法用格式化制作字符串对象?
  • @Eonil 是的。首先,您可以像上面的std::cout 一样使用std::ostringstream 对象。当你用内容填充它时(使用&lt;&lt;-operator),你可以使用它的.str() 函数来获取格式化的字符串。 boost::format 无论如何都会返回一个字符串。我没有在答案中包含这个,因为你的问题是关于std::cout
  • 另请参阅fmtlib/fmt 以获取另一种强大的 C++ 格式库。
【解决方案2】:

在 C++20 中,您将能够使用 std::format 进行安全的 printf 类格式设置:

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

除了{fmt} librarystd::format基于,提供了print结合格式化和输出的功能:

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

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

【讨论】:

  • 感谢您在这里的工作。我总是对使用左移运算符进行 IO 的概念感到困惑。 C 有很多 C++ 希望解决的问题,但是当他们完全放弃格式字符串时,他们真的把婴儿扔到了浴缸里(是的,我知道 printf 一直都在那里,但是编译器本身判断 你在 C++ 中使用 libc)。
  • @Roflcopter4 C++ 仍然有字符串格式,但不是真正的“printf”样式格式。您使用 std::xxx 值来指定宽度、输出格式(对于数字)、有效数字和小数位,这是 printf 的 90%。它非常冗长,但想法是输出流将保持“格式化状态”。从来没有真正被认为是一个“好主意”,std::format 之类的东西填补了兼容性空白,但仍然存在 C++20 之前的格式,只是不是很多人喜欢的“形式”。
  • 似乎很遗憾fmt::print() 没有标准化。 fmt 很棒:灵活、简单、安全和快速(特别是对于像花车这样的东西,现在在引擎盖下有 dragonbox(?))。然后我们必须std::cout &lt;&lt; std::format(...); ??我做了一个简单的测试,fmt::print(...) 立即快了 10-15%(还有fmt::print(std::FILE,..) 用于其他任何事情)。那是std::ios::sync_with_stdio(false); 任何解决方案?
  • 我们正在努力,希望 std::print 能够实现 C++23。
【解决方案3】:

这是我习惯的成语。希望对您有所帮助:

// Hacky but idiomatic printf style syntax with c++ <<

#include <cstdlib> // for sprintf

char buf[1024]; sprintf(buf, "%d score and %d years ago", 4, 7);
cout << string(buf) <<endl;

&

【讨论】:

  • 对我来说这是最好的。熟悉上下文,并将我的 C 代码简单更改为 C++。它把cstdlib 用于它的用途。
  • Ofc 直到你发布你的代码之后才会溢出,对吧,哈哈,所以最好使用像 snprintf 这样更安全的东西(注意事项:参见stackoverflow.com/a/14503914/7409029
  • sprintf(或std::sprintf)函数存在于&lt;cstdio&gt;而不是&lt;cstdlib&gt;
【解决方案4】:

我建议使用 ostringstream 而不是 ostream 请参阅以下示例:

#include <vector>
#include <string>
#include <iostream>
#include "CppUnitTest.h"

#define _CRT_NO_VA_START_VALIDATION

std::string format(const std::string& format, ...)
{
    va_list args;
    va_start(args, format);
    size_t len = std::vsnprintf(NULL, 0, format.c_str(), args);
    va_end(args);
    std::vector<char> vec(len + 1);
    va_start(args, format);
    std::vsnprintf(&vec[0], len + 1, format.c_str(), args);
    va_end(args);
    return &vec[0];
}

示例用法:

std::ostringstream ss;
ss << format("%s => %d", "Version", Version) << std::endl;
Logger::WriteMessage(ss.str().c_str()); // write to unit test output
std::cout << ss.str() << std::endl; // write to standard output

【讨论】:

  • 这是我认为的最佳答案。使用 vsnprintf 来获取“要写入的字符数”,但不允许它写任何东西是天才。我做了一个修改...我直接使用了字符数组而不是向量 (char vec[len + 1];)。认为它的开销较小。然后返回字符数组(return vec;)。
  • 这不是最好的答案。正如@JoeDimig 所说,return &amp;vec[0](或更明智的return {vec.data(), vec.size()})将无缘无故地复制整个字符串。
【解决方案5】:

要实现 printf,可以使用 c++11 模板参数:

#include <iostream>
#include <string>

inline std::ostream & mprintf(std::ostream & ostr, const char * fstr) throw()
{
    return ostr << fstr;
}

template<typename T, typename... Args> 
std::ostream & mprintf(std::ostream & ostr, 
        const char * fstr, const T & x) throw()
{
    size_t i=0;
    char c = fstr[0];

    while (c != '%')
    {
        if(c == 0) return ostr; // string is finished
        ostr << c;
        c = fstr[++i];
    };
    c = fstr[++i];
    ostr << x;

    if(c==0) return ostr; // 

    // print the rest of the stirng
    ostr << &fstr[++i];
    return ostr;
}


template<typename T, typename... Args> 
std::ostream & mprintf(std::ostream & ostr,
        const char * fstr, const T & x, Args... args) throw()
{
    size_t i=0;
    char c = fstr[0];

    while (c != '%')
    {
        if(c == 0) return ostr; // string is finished
        ostr << c;
        c = fstr[++i];
    };
    c = fstr[++i];
    ostr << x;

    if(c==0) return ostr; // string is finished

    return mprintf(ostr, &fstr[++i], args...);
}

int main()
{
    int c = 50*6;
    double a = 34./67.;
    std::string q = "Hello!";

    // put only two arguments
    // the symbol after % does not matter at all
    mprintf(std::cout, "%f + %f = %a \n", c, a);

    // print string object: for real printf one should write q.c_str() 
    mprintf(std::cout, "message: \"%s\". \n", q);

    // the last argument will be ignored
    mprintf(std::cout, "%z + %f\n", (long)a, 12, 544 );

}

输出

300 + 2 = %a 
message: "Hello!". 
2 + 12

这是一个非常简单的代码,可以改进。

1) 优点是它使用 输出的任意参数

2) 它忽略格式化字符串中参数的类型:在 % 之后可以代表任意符号,甚至是空格。输出流决定如何打印相应的对象。它还兼容 printf。

3) 缺点是不能打印百分号'%',需要稍微改进一下代码。

4) 无法打印格式化数字,如 %4.5f

5) 如果参数的数量少于格式化字符串所预测的数量,则该函数只打印字符串的其余部分。

6) 如果参数的数量大于格式化字符串预测的数量,则忽略剩余的参数

可以改进代码以使 2)-6) 完全模仿 printf 行为。 但是,如果你遵循 printf 的规则,那么基本上只需要修复 3) 和 4)。

【讨论】:

    【解决方案6】:

    字段宽度

    设置字段宽度非常简单。对于每个变量,只需在其前面加上“setw(n)”。像这样:

    #include <iostream>
    #include <iomanip>
    
    using namespace std;
    
    int main()
    {
      const int max = 12;
      const int width = 6;
      for(int row = 1; row <= max; row++) {
          for(int col = 1; col <= max; col++) {
              cout << setw(width) << row * col;
          }
          cout << endl;
      }
      return 0;
    }
    

    注意“setw(n)”是如何控制字段宽度的,所以每个数字都是 打印在保持相同宽度的字段内,无论 数字本身的宽度。

    -- 来自"Programming/C++ tutorial" by P. Lutus

    【讨论】:

    • 如果整个答案取自本教程,则应使用引号格式说明。
    【解决方案7】:

    我自己写的,但想出了类似于 user3283405 的答案

    我的解决方案使用 vasprintf() 来实现格式化,并使用 std::ostream 的

    用法:

    std::cout << putf(const char *format, ...); //Same format as C printf(3)
    

    代码:

    #define _GNU_SOURCE
    #include <cstdarg>
    #include <iostream>
    #include <cstdio>
    
    struct putf_r{
            char *s;
    };
    
    putf_r putf(const char *fmt, ...){
            va_list ap;
            va_start(ap, fmt);
            putf_r a;
    #pragma GCC diagnostic push
    #pragma GCC diagnostic ignored "-Wformat-nonliteral"
            vasprintf(&a.s, fmt, ap);
    #pragma GCC diagnostic pop
            va_end(ap);
            return a;
    }
    
    std::ostream& operator<<(std::ostream& os, putf_r a){
            os<<a.s;
            free(a.s);
            return os;
    }
    
    int main(){
            std::cout << putf("%3d\n", 23) << putf("%a\n", 256.);
    }
    

    请注意,编译器不会检查 putf() 中的格式,因此编译器标志 -Wformat-nonliteral 不会对 putf() 中的可疑代码发出警告,您需要自己处理 uncontrolled format string 问题。
    详细信息可见GitHub

    【讨论】:

    • GCC 和可能 clang 具有注释 printf 之类的函数的属性
    【解决方案8】:

    样本输出:

    2017-12-20T16:24:47,604144+01:00 Hello, World!
    

    代码(在 put_timestamp 中演示了 put_printf 的用法):

    #include <assert.h>
    #include <chrono>
    #include <iomanip>
    #include <iostream>
    
    class put_printf {
        static constexpr size_t failed = std::numeric_limits<size_t>::max(); // for any explicit error handling
        size_t stream_size; // excluding '\0'; on error set to 0 or to "failed"
        char buf_stack[2048+1]; // MAY be any size that fits on the stack (even 0), SHOULD be (just) large enough for most uses (including '\0')
        std::unique_ptr<char[]> buf_heap; // only used if the output doesn't fit in buf_stack
    public:
        explicit put_printf(const char *format, ...)
                #if __GNUC__
                __attribute__ ((format (printf, 2, 3))) // most compelling reason for not using a variadic template; parameter 1 is implied "this"
                #endif
                {
            va_list args;
            va_start(args, format);
            const int res = vsnprintf(buf_stack, sizeof(buf_stack), format, args);
            va_end(args);
            if (res < 0) { // easily provoked, e.g., with "%02147483646i\n", i.e., more than INT_MAX-1 significant characters (only observed, no guarantee seen)
                stream_size = failed;
            } else if (res < sizeof(buf_stack)) { // preferred path
                stream_size = res;
            } else { // not artificially constrained
                try {
                    const size_t buf_size = static_cast<size_t>(res) + 1; // avoids relying on "res < INT_MAX" (only observed, no guarantee seen)
                    buf_heap.reset(new char[buf_size]); // observed to work even beyond INT_MAX=2^32-1 bytes
                    va_start(args, format);
                    if (vsnprintf(buf_heap.get(), buf_size, format, args) == res) stream_size = res;
                    else stream_size = failed; // can't happen
                    va_end(args);
                } catch (const std::bad_alloc&) { // insufficient free heap space (or an environment-specific constraint?)
                    stream_size = failed;
                }
            }
        }
        friend std::ostream& operator<<(std::ostream& os, const put_printf& self) {
            if (self.stream_size == failed) {
                // (placeholder for any explicit error handling)
                return os;
            } else {
                // using write() rather than operator<<() to avoid a separate scan for '\0' or unintentional truncation at any internal '\0' character
                return os.write((self.buf_heap ? self.buf_heap.get() : self.buf_stack), self.stream_size);
            }
        }
    };
    
    class put_timestamp {
        const bool basic = false;
        const bool local = true;
    public:
        friend std::ostream& operator<<(std::ostream& os, const put_timestamp& self) {
            const auto now = std::chrono::system_clock::now();
            const std::time_t now_time_t = std::chrono::system_clock::to_time_t(now);
            struct tm tm; if ((self.local ? localtime_r(&now_time_t, &tm) : gmtime_r(&now_time_t, &tm)) == nullptr) return os; // TODO: explicit error handling?
            static_assert(4 <= sizeof(int), "");
            const int microseconds = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch() % std::chrono::seconds(1)).count();
            assert(0 <= microseconds && microseconds < 1000000); // TODO: (how) do we know?
            // TODO: doesn't "point" in "decimal_point()" imply "dot"/"full stop"/"period", unlike an obviously neutral term like "mark"/"separator"/"sign"?
            const char decimal_sign = std::use_facet<std::numpunct<char>>(os.getloc()).decimal_point() == '.' ? '.' : ','; // full stop accepted, comma preferred
            // TODO: all well and good for a locale-specific decimal sign, but couldn't the locale also upset microseconds formatting by grouping digits?
            os << std::put_time(&tm, self.basic ? "%Y%m%dT%H%M%S" : "%FT%T") << put_printf("%c%06i", decimal_sign, microseconds);
            if (! self.local) return os << "Z";
            const int tz_minutes = std::abs(static_cast<int>(tm.tm_gmtoff)) / 60;
            return os << put_printf(self.basic ? "%c%02i%02i" : "%c%02i:%02i", 0 <= tm.tm_gmtoff ? '+' : '-', tz_minutes / 60, tz_minutes % 60);
        }
    };
    
    int main() {
        // testing decimal sign
        ///std::cout.imbue(std::locale("en_GB"));
        ///std::cout.imbue(std::locale("fr_FR"));
    
        std::cout << put_timestamp() << " Hello, World!\n";
        #if 0
        typedef put_printf pf; // just to demo local abbreviation
        std::cout << "1: " << pf("%02147483646i\n"  , 1     ) << std::endl; // res < 0
        std::cout << "2: " << pf("%02147483643i%i\n", 1, 100) << std::endl; // res < 0
        std::cout << "3: " << pf("%02147483643i%i\n", 1,  10) << std::endl; // works
        std::cout << "4: " << pf("%02147483646i"    , 1     ) << std::endl; // works
        #endif
        return 0;
    }
    

    关于 put_printf 的评论:

    // Reasons for the name "put_printf" (and not "putf" after all):
    // - put_printf is self-documenting, while using the naming pattern also seen in std::put_time;
    // - it is not clear whether the proposed std::putf would support exactly the same format syntax;
    // - it has a niche purpose, so a longer name is not an objection, and for frequent local uses
    //     it is easy enough to declare an even shorter "typedef put_printf pf;" or so.
    // Evaluation of delegating to vsnprintf() with intermediate buffer:
    // (+) identical result without implementation and/or maintenance issues,
    // (?) succeeds or fails as a whole, no output of successful prefix before point of failure
    // (-) (total output size limited to INT_MAX-1)
    // (-) overhead (TODO: optimal buf_stack size considering cache and VM page locality?)
    // Error handling (an STL design problem?):
    // - std::cout.setstate(std::ios_base::failbit) discards further std::cout output (stdout still works),
    //     so, to be aware of an error in business logic yet keep on trucking in diagnostics,
    //     should there be separate classes, or a possibility to plug in an error handler, or what?
    // - should the basic or default error handling print a diagnostic message? throw an exception?
    // TODO: could a function "int ostream_printf(std::ostream& os, const char *format, ...)"
    //           first try to write directly into os.rdbuf() before using buf_stack and buf_heap,
    //           and would that significantly improve performance or not?
    

    【讨论】:

    • 您的代码不可读。请抽象出您对空格和换行符的明显过敏。
    【解决方案9】:

    当我需要 cout 的类型安全性和 printf() 简单变量的快速简便格式化时,我会像这样混合两者。这是一个丑陋的修复,但是当我需要输出诸如“02/07/2014 10:05am”之类的内容以及一些更复杂的实体时,它可以为我完成任务:

    #include <stdio>
    #include <stdarg>
    #include <stdlib>
    #include <iostream>
    
    #pragma hdrstop
    
    using namespace std;
    
    
    char* print(char* fmt, ...)
    {
        static char buffer[80] = "";
    
        va_list argptr;
        va_start(argptr,fmt);
    
        vsprintf(buffer, fmt, argptr);
    
        va_end(argptr);
    
        return buffer;
    }
    
    #pragma argsused
    int main(int argc, char* argv[])
    {
    
    cout << print("\n%06d\n%6d\n%6d\n%010.3f",1,12,123,123.456);
    
    system("PAUSE>NUL");
    
    return 0;
    
    }
    

    【讨论】:

    • 至少使用vsnprintf 来避免最明显的错误:缓冲区溢出。
    猜你喜欢
    • 1970-01-01
    • 2011-08-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-05
    • 2017-09-29
    • 2013-01-28
    相关资源
    最近更新 更多