遨遊於C++世界時,最討厭的當屬於對c-style的兼容🙄。html
在格式化字符串時,一般使用的是snprintf
這個c函數。snprintf
是sprintf
的安全版,可以避免緩衝區溢出。c++
char buf[1024] = {0}; std::string s = "Hello world"; snprintf(buf, sizeof(buf), "format str: %s", s.c_str());
snprintf
接受的參數跟printf
差很少,都是c-style的數據類型,如%s
接受的是const char*
類型的數據,這就須要咱們將std::string
作一個轉換。這一個小小的轉換讓我以爲很是不爽,有沒有可能讓std::string
在作某個函數的參數時能自動作轉換?甚至讓一個將一個普通的對象自動轉換成const char*
類型?git
接下來咱們將利用c++11的可變參數模板(variadic templates)、參數包(parameters pack)、完美轉發(perfect forwarding)等特性來實現這一想法。github
寫C++代碼時,我滿腦子都是怎麼最大限度地提升性能。咱們此次的目標也同樣,在提供方便的同時,不要對性能有太大影響,甚至不影響。安全
首先是要將傳入fmt
函數的參數完美轉發至snprintf
。bash
template<typename... Args> string fmt(const char *format, Args&&... args) { char buf[128] = {0}; snprintf(buf, sizeof(buf), format, convert(std::forward<Args>(args))...); return buf; }
這是一個可變參數模板,args表示傳入的參數們。咱們的思路是將傳入的參數作一個轉換以後傳給snprintf
。函數
爲了原封不動的保持左右值引用,首先是用Args&&
代替Args
的參數類型,此處模板函數的Args須要編譯器推導,因此是一個通用引用(Universal reference),能夠指代左值或右值。用std::forward<Args>
能保持參數的左右值性質,作到參數的完美轉發。post
在convert
這個函數中,咱們要將特定的類型轉換成const char*
類型,而那些能被snprintf
接受的類型如int
, double
, char*
,則原封不動的返回。性能
convert
函數針對不一樣的參數類型須要返回不一樣的類型。這裏也將返回值做爲一個模板類型便可。c++11
template<typename T> struct item_return { using type = T&&; };
convert
函數的定義爲:
template<typename T> inline typename item_return<T>::type convert(T&& arg) { return static_cast<T&&>(arg); }
convert函數默認將傳入的參數原封不動的返回。接下來咱們要作模板的偏特化,對於指定的對象,將其轉換爲const char *類型
// lvalue template<> struct item_return<obj&> { using type = const char*; }; template<> inline typename item_return<obj&>::type convert<obj&>(obj &arg) { std::cout << "receive lvalue\n"; return arg.s.c_str(); } // rvalue template<> struct item_return<obj> { using type = const char*; }; template<> inline typename item_return<obj>::type convert<obj>(obj &&arg) { std::cout << "receive rvalue\n"; return arg.s.c_str(); }
注意,返回值也是須要偏特化的。
我構造了一個class,hook他的兩個構造函數以便於觀察是否發生了拷貝。
class obj { public: string s; obj(const char * ss) { s = ss; } obj(const obj& other):s(other.s) { printf("copy constructor\n"); } obj(obj&& other):s(other.s) { printf("move constructor\n"); other.s.clear(); } };
以後咱們使用fmt函數,就能像格式化c-style字符串同樣,格式化任意一個對象啦。
int main() { obj a("haha"); int b = 3; std::cout << fmt("%s %s\n%d %d", a, obj("xixi"), b, 2) << std::endl; return 0; }
運行結果爲
receive lvalue receive rvalue haha xixi 3 2
很好,並無發生拷貝。