1

I don't see a constructor for std::string that can consume a va_list. Is there a common solution for converting a va_list to a std::string?

I've seen solutions in the form of:

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

  va_list args;
  va_start(args, format);

  char buffer[1024];
  vsnprintf(buffer, sizeof(buffer), format, args);
  result = std::string(buffer);

  va_end(args);
  return result;
}

This feels error prone and hacky. Is there a way for std::string to be constructed from or operate on a va_list directly?

NOTE: The main problem I have with the solution above is the need to guess at the amount of memory I need. I don't want to waste too much or not have enough. Ideally, I would like a std::string style opaque allocation that just works.

NOTE: I need a solution that does not require third party library support.

Community
  • 1
  • 1
Zak
  • 12,213
  • 21
  • 59
  • 105
  • 1
    FWIW, you never want to return a reference from a function unless the thing you are returning is marked as static. – NathanOliver Mar 29 '19 at 20:03
  • This is not a duplicate. The answer to that question is to use `boost` or the `fmt` library. Neither of which I can use. – Zak Mar 29 '19 at 20:05
  • @NathanOliver Good point. I removed that, as it doesn't add to my question. – Zak Mar 29 '19 at 20:06
  • @Nathan _"you never want to return a reference"_ _You mustn't_ seems to be the more appropriate phrase for such case. – πάντα ῥεῖ Mar 29 '19 at 20:06
  • @Zak _"This is not a duplicate."_ Well, the ball is yours now, just act with it. – πάντα ῥεῖ Mar 29 '19 at 20:07
  • Again, it was superfluous and has been removed. @πάνταῥεῖ the answer you have marked as duplicate is unsatisfactory. – Zak Mar 29 '19 at 20:07
  • @Zak _"is unsatisfactory."_ [Elaborate your question](https://stackoverflow.com/posts/55424746/edit) why exactly. – πάντα ῥεῖ Mar 29 '19 at 20:09
  • @NathanOliver There are also cases when you return an otherwise owned objects, such as `operator[]` often does, an `optional`'s `value`, etc. Edit : Many typos. – François Andrieux Mar 29 '19 at 20:13
  • @πάνταῥεῖ done. Now please unflag the question. – Zak Mar 29 '19 at 20:14
  • 3
    @Zak One of that duplicate's answers provides a solution with no third party libraries. – François Andrieux Mar 29 '19 at 20:15
  • 1
    My usual approach to formatting a string is to simply use [std::ostringstream](http://www.cplusplus.com/reference/sstream/ostringstream/) – selbie Mar 29 '19 at 20:16
  • @FrançoisAndrieux #1 uses `boost` or a less trivial version of what I have posted above, while #2 uses `fmt`. Again, those answers are unsatisfactory. – Zak Mar 29 '19 at 20:17
  • 1
    @Zak There is a version that relies on `snprinf` only - second code piece of the isarandi's answer. –  Mar 29 '19 at 20:19
  • @Zak _"Now please unflag the question"_ Others seem to have done so already. That kind of demand isn't very kind anyways. – πάντα ῥεῖ Mar 29 '19 at 20:19
  • If you dont' want to use header-only thirdparty, then just copy-paste their code into your project. – Dmitry Sazonov Mar 29 '19 at 20:20
  • @πάνταῥεῖ I was happy to clarify. My preference is to let this go, but you have now put me in a position where I feel the need to explain myself. It would have been nice if you had asked me to do clarify my question, before you flagged it as a duplicate. Also, you could have waited more than 30 seconds before flagging as duplicate. – Zak Mar 29 '19 at 20:24
  • @selbie Can you elaborate on your approach? I am unfamiliar with it. – Zak Mar 29 '19 at 20:26
  • @Zak _"Also, you could have waited more than 30 seconds before flagging as duplicate."_ Well, time goes fast here, just like as coming near to _black holes_ ;-). – πάντα ῥεῖ Mar 29 '19 at 20:29
  • Is there a specific reason why you cannot use [`std::ostringstream`](https://en.cppreference.com/w/cpp/io/basic_ostringstream) in your case? – Michael Kenzel Mar 29 '19 at 20:33
  • @MichaelKenzel Because I am unfamiliar with that technique. Can you propose your solution as an answer? @selbie also mentioned `std::ostringstream`, I have no idea how you would do this. – Zak Mar 29 '19 at 21:19
  • `stringstream ss; ss << "There are " << count << " ways to eat " << item << " that cost less than " << amount; string str = ss.str();` – selbie Mar 29 '19 at 22:07
  • So to apply this to my problem, you are suggesting using `va_arg(args, n)`? – Zak Mar 29 '19 at 22:09
  • How would you handle breaking apart the `const char * format` string? Can you propose this method as an actual answer? – Zak Mar 29 '19 at 22:10

2 Answers2

5

vsnprintf() can calculate the needed buffer size without actually outputting to a buffer, so you usually don't need a separate char[] at all, you can just calculate the size, allocate the std::string to that size, and then use the std::string's own internal buffer for the output, eg:

std::string vstring (const char * format, ...)
{
  std::string result;
  va_list args, args_copy;

  va_start(args, format);
  va_copy(args_copy, args);

  int len = vsnprintf(nullptr, 0, format, args);
  if (len < 0) {
    va_end(args_copy);
    va_end(args);
    throw std::runtime_error("vsnprintf error");
  }

  if (len > 0) {
    result.resize(len);
    // note: &result[0] is *guaranteed* only in C++11 and later
    // to point to a buffer of contiguous memory with room for a
    // null-terminator, but this "works" in earlier versions
    // in *most* common implementations as well...
    vsnprintf(&result[0], len+1, format, args_copy); // or result.data() in C++17 and later...
  }

  va_end(args_copy);
  va_end(args);

  return result;
}

Though, prior to C++11, using a separate buffer would be a more "correct" (ie, portable) and safer choice, eg:

std::string vstring (const char * format, ...)
{
  std::string result;
  va_list args, args_copy;

  va_start(args, format);
  va_copy(args_copy, args);

  int len = vsnprintf(nullptr, 0, format, args);
  if (len < 0) {
    va_end(args_copy);
    va_end(args);
    throw std::runtime_error("vsnprintf error");
  }

  if (len > 0) {
    std::vector<char> buffer(len+1);
    vsnprintf(&buffer[0], buffer.size(), format, args_copy);
    result = std::string(&buffer[0], len);
  }

  va_end(args_copy);
  va_end(args);

  return result;
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • You are writing beyond `result` buffer because of `len + 1`. `snprintf` is allowed and will modify every byte in a buffer on truncation. –  Mar 29 '19 at 20:35
  • 2
    You are also reusing `args` which is illegal and won't work on some platforms (x86_64 Windows for example). –  Mar 29 '19 at 20:36
  • Yep there's a severe scarcity of `va_copy` in this code. – Matteo Italia Mar 29 '19 at 20:38
  • @StaceyGirl actually, I'm not writing beyond `result`. `vsnprintf()` returns a length that does not include the null terminator, and `std::string::resize()` allocates memory that includes room for a null terminator, but the buffer size passed into `vsnprintf()` must be +1 to include the null terminator as `vsnprintf()` outputs `size-1` formatted characters followed by a null terminator. – Remy Lebeau Mar 29 '19 at 20:41
  • @MatteoItalia I added `va_copy()` – Remy Lebeau Mar 29 '19 at 20:49
  • @StaceyGirl I am not accessing `result[0]` when `len` is 0 (`if (len > 0) { ... }`). But `vsnprintf()` allows `buf=null` and `bufsz=0` when calculating a buffer size. – Remy Lebeau Mar 29 '19 at 20:49
3

You can use the fact that snprintf can be used with nullptr buffer and size 0 to get resulting buffer size and write the message into the std::string itself.

Note that va_copy should be used if you want to reuse va_list.

std::string vformat(const char *format, va_list args)
{
    va_list copy;
    va_copy(copy, args);
    int len = std::vsnprintf(nullptr, 0, format, copy);
    va_end(copy);

    if (len >= 0) {
        std::string s(std::size_t(len) + 1, '\0');
        std::vsnprintf(&s[0], s.size(), format, args);
        s.resize(len);
        return s;
    }

    const auto err = errno;
    const auto ec = std::error_code(err, std::generic_category());
    throw std::system_error(ec);
}

std::string format(const char *format, ...)
{
    va_list args;
    va_start(args, format);
    const auto s = vformat(format, args);
    va_end(args);
    return s;
}