C++ printf-like std::string

想要拼接字串,但是又不想要用 std::iostream<< 拼接。覺得 sprintf 的方式雖然 type-unsafe ,但是輕量、快、又好用。想用 sprintf,可是只能使用 char[] ,而且每次都先定義變數 char buf[256] = {} ,或是 char buf* = new char[256]再傳入,這樣多 call 一行有點不方便。遇到 buffer 長度不足的可能情況,你還要去檢查 sprintf 的回傳值,來做 error handling。

使用 format_str,可以將麻煩的 char 字串定義錯誤檢查重新 allocation 的動作,給包裝起來。

演譯版的 format_str

std::string format_str_simple(const char* format, ...)
{
  const static BUF_SIZE = 256;
  va_list args;
  va_start(args, format);
  char buf[BUF_SIZE] = {};
  vsprintf(buf,  format, args);
  va_end(args);
  return buf;
}

安全版 format_str

先假設大家 format 一個字串,長度很少會用超過 256,超出再使用 allocation 。

  • 極低機率才用 Memory Allocation
  • 一律用 256 長度,會浪費空間。
std::string format_str_safe(const char* format, ...)
{
  const static BUF_SIZE = 256;
  va_list args;
  va_start(args, format);
  char buf[BUF_SIZE] = {};
  int nWouldWritten = vsnprintf(buf, BUF_SIZE, format, args); // since c++11, since VS 2012
  assert(nWouldWritten >= 0 && "error in format_str_safe::vsnprintf.");
  if(nWouldWritten > BUF_SIZE)
  {
      std::unique_ptr<char[]> buf2(new char[nWouldWritten + 1]());      
      int nWouldWritten2 = vsnprintf(buf2, nWouldWritten + 1, format, args);      
      assert(nWouldWritten2 >= 0 && "error in format_str_safe::vsnprintf.");
      va_end(args);
      return buf2.get();
  }
  va_end(args);
  return buf;
}

動態空間版 format_str

一律先問 buf size ,再 alloc 空間裝字串。

  • Memory Alloction 有 overhead
  • 不浪費空間
std::string format_str_safe_dynamic(const char* format, ...)
{
  va_list args;
  va_start(args, format);
  int nActualSize = vsnprintf(0, 0, format, args);
  assert(nActualSize >= 0 && "error in format_str_safe_dynamic::vsnprintf.");
  std::unique_ptr<char[]> buf(new char[nActualSize + 1]());
  int nActualSize2 = vsnprintf(buf.get(), nActualSize + 1, format, args); 
  assert(nActualSize2 >= 0 && "error in format_str_safe_dynamic::vsnprintf.");
  va_end(args);
  return buf.get();
}

Error hanlding

error handling 的作法可以定 assert return code、拋 excpetion、或只回空字串。不過這種 type-unsafe 的 vnsprintf 真的炸在 compiler 無法偵測的時候,可能也沒辦法處理。

舊作法例子 1

  • 要先定義 buf 。
  • 多一個 char[256] 的暫時變數在 scope 裡。
  • 很容易忘記或很懶得使用 snprintf 版,而去用 sprintf
  • buf_size 256,在定義時,跟 snprintf 裡重覆了,感覺很差。
  • 很容易忘記很懶得做 error handling。
  • 最經典的作法。
char buf[256] = {};
int nWritten = snprintf(buf, 256, "%s has %d %s(s).", "John", 2, "Apple"); 
/* ... Error handling ... */
/* ... Assertion ... */
/* ... Rellocation ... */
std::string msg = buf;

舊作法例子 2

  • 字串組成比較醜,沒有 printf 用 format 方式來得清楚明白。
  • 字串以外的 type 轉換 要自已來,C++ 03 的使用者沒有 std::to_string(),或是要自已用 atoi 類的。
std::string msg = std::string("John") + " has " + std::to_string(2) + " Apple.";

舊作法例子 3

  • std::iostream 很肥。
  • 字串組成沒有 printf format 方式來得清楚。
  • std::iostream很多人討厭用。
  • iostream 具有 type-safe,compiler 能感知參數傳遞的錯誤。
std::ostringstream oss;
oss << "John" << " has " << 2 << " Apple.";
std::string msg = oss.str();
使用後
std::string msg = format_str("%s has %d %s(s).", "John", 2, "Apple");

只是一個 handy tool

使用 format_str 並沒有去除掉 sprintf type-unsafe 的風險。在傳參數的時候傳錯,或是 format 中的 escape 沒對好,也是會炸開。基本上 format_str 只是一個幫你省去用 sprintf 時,需要做的前處理(定義 char),跟後處理(error handling) 的一個方便的 tool 而已。

wide char 版

  • std::stringstd::wstring
  • const char*const wchar_t*
  • char buf[BUF_SIZE]wchar_t buf[BUF_SIZE]
  • vsnprintfvswprintf

ref

http://en.cppreference.com/w/cpp/io/c/vfprintf

comments powered by Disqus