C++ generates formatted standard string instance code

  • 2020-06-19 11:18:56
  • OfStack

Two ways to format strings

As we all know, C++ std::string Features are incomplete, lacking all kinds of features, such as formatting strings.

In python3, two methods of formatting strings are supported: one is the C style, with the formatted part beginning with %, the corresponding specific type after % (for example, %s corresponds to string %d corresponds to integer), and the other is the type-independent style, {0} corresponds to the first parameter, {1} corresponds to the second parameter.


>>> "{0}'s age is {1}".format(" Latosolic red ", 11)
" Latosolic red 's age is 11"
>>> "%s's age is %d" % (" Latosolic red ", 11)
" Latosolic red 's age is 11"

In C++, you can only borrow the C function and use snprintf to format 1 buffer


#define BUFFSIZE 512
 char buf[BUFFSIZE];
 snprintf(buf, BUFFSIZE, "%s's age is %d\n", " Latosolic red ", 11);

Or use type independent flow operators


 std::ostringstream os;
 os << " Latosolic red " << "'s age is " << 11 << "\n";
 std::string s = os.str();

Efficiency aside, this is useful < < Splicing multiple different types of objects is a lot of code, and it's more difficult to control the exact output format, such as the number of digits, or the number of decimal places. At least it's too complicated for me to remember, preferring C style snprintf for control. Such as


 double d = 3.1415926;
 snprintf(buf, BUFFSIZE, " PI : %-8.3lf It was discovered by Zu Chongzhi \n", d);

$ ./a.out 
 PI : 3.142  It was discovered by Zu Chongzhi 

With %-8.3lf, the float of type lf(long float) is set to 8, the decimal point is set to 3, and the minus sign indicates left-aligned, which is a very simple and compact representation.

As for the C++ iomanip header file implementation, I spent some time looking it up.


 double d = 3.1415926;
 os << " PI : " << std::setw(8) << std::fixed
  << std::setprecision(3) << std::left
  << d << " It was discovered by Zu Chongzhi \n";

Except that the code is so long and can be missed std::fixed In addition, the problem is that setprecision has changed the default Settings, that is, if os again < < Passing in a floating point number still preserves three decimal places.

One might say that this benefit is that setprecision and setw can receive one variable and be very large. In fact, the snprintf1 sample can do this.


 double d = 3.1415926;
 int n1 = 8, n2 = 3;
 snprintf(buf, BUFFSIZE, " PI : %-*.*lf It was discovered by Zu Chongzhi \n", n1, n2, d);

C++ wraps snprintf to generate formatted std::string objects

In APUE UNP TLPI these books on C programming under Linux, have written their own error handling library to wrap snprintf to produce formatted output, so as not to repeatedly define buffer/call snprintf and so on.

One of the disadvantages of this approach is that the buffer (character array) length is limited, but generally buffer size is defined large enough to be sufficient, after all, printing too long formatted strings is not as good as calling the function several times.

On the other hand, because these functions simply print information, especially often after printing information directly out of the program. So no error string is returned. These functions are not sufficient if you want to pass the error message as an exception to the previous layer in C++. So I need to make some simple changes.


inline std::string format_string(const char* format, va_list args) {
 constexpr size_t oldlen = BUFSIZ;
 char buffer[oldlen]; //  Buffer on the default stack 
 va_list argscopy;
 va_copy(argscopy, args);
 size_t newlen = vsnprintf(&buffer[0], oldlen, format, args) + 1;
 newlen++; //  Count the terminator '\0'
 if (newlen > oldlen) { //  The default buffer is not large enough to be allocated from the heap 
  std::vector<char> newbuffer(newlen);
  vsnprintf(newbuffer.data(), newlen, format, argscopy);
  return newbuffer.data();
 }
 return buffer;
}

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

 return s;
}

This is an implementation modeled after UNP, with the parameters defined as va_list and... The version that accepts va_list is also available for other functions. Because of the C-style variable parameter list... Cannot be passed as a parameter. On the other hand, the va_list type does not necessarily have a copy constructor, so va_copy has to copy one copy of va_list for the second use.

C++11 adds a variable template parameter feature that simplifies the above code


template <typename ...Args>
inline std::string format_string(const char* format, Args... args) {
  constexpr size_t oldlen = BUFSIZ;
  char buffer[oldlen]; //  Buffer on the default stack 

  size_t newlen = snprintf(&buffer[0], oldlen, format, args...);
  newlen++; //  Count the terminator '\0'

  if (newlen > oldlen) { //  The default buffer is not large enough to be allocated from the heap 
    std::vector<char> newbuffer(newlen);
    snprintf(newbuffer.data(), newlen, format, args...);
    return std::string(newbuffer.data());
  }

  return buffer;
}

Passing variable template parameters is also made 10 points easier (perfect forwarding with forward), as shown below


xyz@ubuntu:~/unp_practice/lib$ cat test.cc 
#include <string.h>
#include <unistd.h>
#include "format_string.h"

template <typename ...Args>
void errExit(const char* format, Args... args) {
  auto errmsg = format_string(format, std::forward<Args>(args)...);
  errmsg = errmsg + ": " + strerror(errno) + "\n";
  fputs(errmsg.c_str(), stderr);
  exit(1);
}

int main() {
  const char* s = "hello world!";
  int fd = -1;
  if (write(fd, s, strlen(s)) == -1)
    errExit("write \"%s\" to file descriptor(%d) failed", s, fd);
  return 0;
}
xyz@ubuntu:~/unp_practice/lib$ g++ test.cc -std=c++11
xyz@ubuntu:~/unp_practice/lib$ ./a.out 
write "hello world!" to file descriptor(-1) failed: Bad file descriptor

conclusion


Related articles: