20

I am working with an open-source UNIX tool that is implemented in C++, and I need to change some code to get it to do what I want. I would like to make the smallest possible change in hopes of getting my patch accepted upstream. Solutions that are implementable in standard C++ and do not create more external dependencies are preferred.

Here is my problem. I have a C++ class -- let's call it "A" -- that currently uses fprintf() to print its heavily formatted data structures to a file pointer. In its print function, it also recursively calls the identically defined print functions of several member classes ("B" is an example). There is another class C that has a member std::string "foo" that needs to be set to the print() results of an instance of A. Think of it as a to_str() member function for A.

In pseudocode:

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());

I should mention that C is fairly stable, but A and B (and the rest of A's dependents) are in a state of flux, so the less code changes necessary the better. The current print(FILE* F) interface also needs to be preserved. I have considered several approaches to implementing A::to_str(), each with advantages and disadvantages:

  1. Change the calls to fprintf() to sprintf()

    • I wouldn't have to rewrite any format strings
    • print() could be reimplemented as: fprint(f, this.to_str());
    • But I would need to manually allocate char[]s, merge a lot of c strings , and finally convert the character array to a std::string
  2. Try to catch the results of a.print() in a string stream

    • I would have to convert all of the format strings to << output format. There are hundreds of fprintf()s to convert :-{
    • print() would have to be rewritten because there is no standard way that I know of to create an output stream from a UNIX file handle (though this guy says it may be possible).
  3. Use Boost's string format library

    • More external dependencies. Yuck.
    • Format's syntax is different enough from printf() to be annoying:

    printf(format_str, args) -> cout << boost::format(format_str) % arg1 % arg2 % etc

  4. Use Qt's QString::asprintf()

    • A different external dependency.

So, have I exhausted all possible options? If so, which do you think is my best bet? If not, what have I overlooked?

Thanks.

feedc0de
  • 3,646
  • 8
  • 30
  • 55
underspecified
  • 979
  • 1
  • 7
  • 15
  • Although I already answered this question, I would also like to point out this project: https://github.com/c42f/tinyformat which solves the problem nicely, and really does a great job of reproducing printf formatting notation. These days, I use that package directly, rather than the vsprintf method I detailed a few years ago in my other answer. – Larry Gritz Jul 15 '14 at 19:09

8 Answers8

43

Here's the idiom I like for making functionality identical to 'sprintf', but returning a std::string, and immune to buffer overflow problems. This code is part of an open source project that I'm writing (BSD license), so everybody feel free to use this as you wish.

#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]);
    }
}

EDIT: when I wrote this code, I had no idea that this required C99 conformance and that Windows (as well as older glibc) had different vsnprintf behavior, in which it returns -1 for failure, rather than a definitive measure of how much space is needed. Here is my revised code, could everybody look it over and if you think it's ok, I will edit again to make that the only cost listed:

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];
    }
}
Michael Anderson
  • 70,661
  • 7
  • 134
  • 187
Larry Gritz
  • 13,331
  • 5
  • 42
  • 42
  • Nice work on format/vformat. Maybe stackoverflow needs some kind of code snippit sharing section :-) – underspecified Sep 16 '08 at 08:36
  • 1
    It seems that your code will not work if the resulting string is larger then 1024 bytes. According to MSDN: vsnprintf - Return Value ... if the number of characters to write is greater than count, these functions return -1 indicating that output has been truncated. – Andreas Apr 22 '09 at 06:22
  • 2
    Why use a variable array size if you're not going to do this in a loop? There's no need to copy 'ap', which could be expensive. The GNU [v]s[n]printf man page has an example of how to do this more portably: http://www.tin.org/bin/man.cgi?section=3&topic=snprintf – Bklyn Aug 25 '09 at 23:16
  • 1
    Also, use the return value from vsnprintf when creating your string. This will save you an extra strlen call. – Bklyn Aug 25 '09 at 23:18
  • Andreas: Thanks so much! I didn't realize that vsnprintf has a different failure return value on Windows. I will edit the code example to reflect this. – Larry Gritz Aug 27 '09 at 17:11
  • Bklyn: thanks for the tips, again related to vsnprintf that wasn't C99. Could you look over my edits and tell me what you think of the new version? – Larry Gritz Aug 27 '09 at 17:47
  • This was (almost) exactly what I needed. Thanks. I did change it to std::string strprintf( const char *fmt, ... ) by adding va_list ap; and va_start (ap, fmt); to the start of the function and va_end(ap) to the end. I broke out of the loop on success (instead of returning right away) so I could return the string after the va_end(). I also made things a little more conservative by changing the stackbuf[] size and the resize() argument to size+2 (not sure if size is ok, size+1 would definitely be ok, size+2 satisfies my paranoia). Tested it all with Visual C++ 2010, made sure I exercised loop. – Bill Forster May 16 '11 at 05:29
  • @Larry, +1 thanks found this and used it with a few mods as indicated in my previous comment (which uses exactly 600 chars, hence this comment:). To test it I started with a tiny automatic buffer and I can confirm that needed is set to -1 on failure with Visual C++ 2010, and that your failure recovery loop works nicely. – Bill Forster May 16 '11 at 05:34
  • I think the fit test should require that needed < size, not less than or equal, because size includes the NUL and needed does not. In case vsnprintf fails for some other reason, you should limit the size-doubling loop to some upperbound. Also, this MSDN reference says that as of VS 2015 and Windows 10, vsnprintf conforms to C99. https://msdn.microsoft.com/en-us/library/1kt27hek.aspx – Ted Feb 23 '16 at 20:37
  • In the version that supports the older versions, I found it necessary to wrap the `vsnprintf` with a `va_copy`, `va_end`. I've updated the code appropriately. – Michael Anderson Apr 08 '16 at 01:31
  • `This code is part of an open source project that I'm writing`... is this library publicly available now? – kevinarpe Aug 15 '16 at 09:55
  • 2
    @kevinarpe It's been available for many years. But I have reproduced the relevant part above. But these days, I strongly recommend using a fully type-safe C++ version, such as tinyformat or other similar projects. – Larry Gritz Aug 16 '16 at 01:17
14

I am using #3: the boost string format library - but I have to admit that I've never had any problem with the differences in format specifications.

Works like a charm for me - and the external dependencies could be worse (a very stable library)

Edited: adding an example how to use boost::format instead of printf:

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

would be something like this with the boost::format library:

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

Hope this helps clarify the usage of boost::format

I've used boost::format as a sprintf / printf replacement in 4 or 5 applications (writing formatted strings to files, or custom output to logfiles) and never had problems with format differences. There may be some (more or less obscure) format specifiers which are differently - but I never had a problem.

In contrast I had some format specifications I couldn't really do with streams (as much as I remember)

bernhardrusch
  • 11,670
  • 12
  • 48
  • 59
  • 1
    Thanks for the clarification on boost::format's usage. It's tempting given that this project already depends on another boost library, but I don't think anything beats a printf that just works with std::string as Loki seems to do. – underspecified Sep 16 '08 at 08:39
  • I tried Loki's SafeFormat, but it turns out it's just swapping boost's 5s for ()s. On a positive note, my code did work once I adopted boost::format :-) – underspecified Sep 18 '08 at 07:29
  • Good to hear that boost::format worked for you - never tried the Loki method. – bernhardrusch Sep 18 '08 at 07:45
3

The {fmt} library provides fmt::sprintf function that performs printf-compatible formatting (including positional arguments according to POSIX specification) and returns the result as std::string:

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

Disclaimer: I'm the author of this library.

vitaut
  • 49,672
  • 25
  • 199
  • 336
2

You can use std::string and iostreams with formatting, such as the setw() call and others in iomanip

Yann Ramin
  • 32,895
  • 3
  • 59
  • 82
1

The following might be an alternative solution:

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

(B::printto similar), and define

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

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

Of course, you should really use snprintf instead of sprintf to avoid buffer overflows. You could also selectively change the more risky sprintfs to << format, to be safer and yet change as little as possible.

Jan de Vos
  • 3,778
  • 1
  • 20
  • 16
  • I will take your reply as a combination of #1 and #2 :-) Does ofstream have a constructor that takes file handles? I was under the impression that they were incompatible ... – underspecified Sep 16 '08 at 08:36
1

You should try the Loki library's SafeFormat header file (http://loki-lib.sourceforge.net/index.php?n=Idioms.Printf). It's similar to boost's string format library, but keeps the syntax of the printf(...) functions.

I hope this helps!

Kevin
  • 25,207
  • 17
  • 54
  • 57
  • Please don't just post some tool or library as an answer. At least demonstrate [how it solves the problem](http://meta.stackoverflow.com/a/251605) in the answer itself. – Baum mit Augen Jul 31 '17 at 18:28
0

Is this about serialization? Or printing proper? If the former, consider boost::serialization as well. It's all about "recursive" serialization of objects and sub-object.

Assaf Lavie
  • 73,079
  • 34
  • 148
  • 203
  • This is about printing proper. C.foo is one piece of data that is eventually shown to the user (largely) as-is. If this were my code, I would lose the print(FILE*) nonsense which is far too restrictive. – underspecified Sep 16 '08 at 08:34
0

Very very late to the party, but here's how I'd attack this problem.

1: Use pipe(2) to open a pipe.

2: Use fdopen(3) to convert the write fd from the pipe to a FILE *.

3: Hand that FILE * to A::print().

4: Use read(2) to pull bufferloads of data, e.g. 1K or more at a time from the read fd.

5: Append each bufferload of data to the target std::string

6: Repeat steps 4 and 5 as needed to complete the task.

dgnuff
  • 3,195
  • 2
  • 18
  • 32