c#中格式化字符串很簡單,比如我們可以這樣格式化一個字符串:
string str = string.format("test {0}, {1}, {2}, {1}, {0} sample", 1, 2.3, "ok");
Console.WriteLine(str);
將輸出:test 1, 2.3, ok, 2.3, 1 sample
這個格式化方法用起來很簡單,支持基本類型的參數,比如int、double和string等,用起來很方便。遺憾的是c++中目前還沒有類似的格式化方法。boost庫提供了一個format方法,但用起來沒有c#的format方法簡單和靈活。讓我們來看看boost.format如何實現上面的格式化:
string str = boost::format("test %1%, %2%, %3%, %4%, %5% sample")%1%2.3%"ok"%2.3%1;
cout<<str<<endl;
boost::format的問題是需要寫很多%,用起來繁瑣又不直觀。c++還缺少一個類似於c#的format方法,要實現一個類似的簡單的format也不難,我將實現一個簡單的format,基本用法和c#一致,為了保持簡單,不支持復雜的功能,只支持基本類型的轉換,轉換的格式控制就不支持了,簡單夠用就好。下面來看看format的具體實現吧:
#include <tuple>
#include <type_traits>
#include <string>
using namespace std;
#include "Variant.hpp"
namespace detail
{
using Value = Variant<uint8_t, uint16_t, uint32_t, uint64_t, int8_t, int16_t, int32_t, int64_t, float, double, string, char*, const char*>;
char g_buf[2000] = {};
template<size_t k, typename Tuple, typename F>
typename std::enable_if < (k == std::tuple_size<Tuple>::value)>::type GetArgByIndex(size_t, Tuple&, F&, char*&)
{
throw std::invalid_argument("arg index out of range");
}
template<size_t k = 0, typename Tuple, typename F>
typename std::enable_if < (k < std::tuple_size<Tuple>::value)>::type GetArgByIndex(size_t index, Tuple& tp, F& f, char*& p)
{
if (k == index)
{
f(p, std::get<k>(tp));
}
else
{
GetArgByIndex<k + 1>(index, tp, f, p);
}
}
inline int GetIndex(char*& p)
{
char temp[3] = {};
int i = 0;
while (*p != '}'&&*p != '\0')
{
if (i >= 2)
throw std::invalid_argument("index is out of range.");
if (std::isdigit(*p))
{
//push digit
temp[i++] = *p;
char next = *(p + 1);
if (std::isdigit(next))
{
temp[i++] = next;
p += 2;
continue;
}
//validate arg
if (!std::isspace(next) && next != '}')
{
throw std::invalid_argument("invalid argument.");
}
}
p++;
}
return i == 0 ? -1 : std::atoi(temp);
}
inline void Fun(char*& buf, Value t)
{
t.Visit([&buf](int i)
{
_itoa(i, buf, 10);
buf += i<10 ? 1 : 2;
},
[&buf](double i)
{
auto r = sprintf(buf, "%.15f", i);
buf += r;
},
[&buf](int64_t i)
{
auto r = sprintf(buf, "%"PRId64, i);
buf += r;
},
[&buf](uint64_t i)
{
auto r = sprintf(buf, "%"PRIu64, i);
buf += r;
},
[&buf](const char* p)
{
int len = strlen(p);
memcpy(buf, p, len);
buf += len;
},
[&buf](string& s)
{
memcpy(buf, s.data(), s.size());
buf += s.size();
});
}
}
template<typename... Args>
inline string format(string& str, Args... args)
{
using namespace detail;
char* buf = g_buf;
auto tp = std::tuple<Args...>(args...);
char* p = (char*) str.c_str();
char* original = p;
int len = str.size() + 1;
int last = 0;
while (*p != '\0')
{
if (*p == '{')
{
//copy content befor {
last = p - original;
memcpy(buf, original, last);
buf += last;
//format args
int index = GetIndex(p);
if (index >= 0)
{
GetArgByIndex<0>(index, tp, Fun, buf);
}
//skip }
original = p + 1;
}
p++;
}
string s = g_buf;
memset(g_buf, 0, buf - g_buf);
return s;
}
再來看看測試代碼:
string str = "it is { 0 }, and {01}, {2}, {01}, {1}";
cout<<format(str, 11, 2.1, "tt")<<endl;
將輸出:it is 11, and 2.1, tt, 2.1 1
用法和c#的一致,比boost的format用起來更方便。這個format支持最多100個參數(0-99),格式化的最長的字符串為2K。上面的實現中用到了Variant,關於Variant的實現可以看前面博文:c++11打造好用的Variant。
如果你發現還有更好的format請告訴我,如果沒有請點一下推薦,謝謝。^_^
c++11 boost技術交流群:296561497,歡迎大家來交流技術。