遨游于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
很好,并无发生拷贝。