遨游于 C ++ 世界时,最讨厌的当属于对 c -style 的兼容????。
在格式化字符串时,通常使用的是 snprintf
这个 c 函数。snprintf
是 sprintf
的安全版,能够避免缓冲区溢出。
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*
类型?
接下来我们将利用 c ++11 的可变参数模板(variadic templates)、参数包(parameters pack)、完美转发(perfect forwarding)等特性来实现这一想法。
参数列表完美转发
写 C ++ 代码时,我满脑子都是怎么最大限度地提高性能。我们这次的目标也一样,在提供方便的同时,不要对性能有太大影响,甚至不影响。
首先是要将传入 fmt
函数的参数完美转发至snprintf
。
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>
能保持参数的左右值性质,做到参数的完美转发。
自动参数转换
在 convert
这个函数中,我们要将特定的类型转换成 const char*
类型,而那些能被 snprintf
接受的类型如int
, double
, char*
,则原封不动的返回。
convert
函数针对不同的参数类型需要返回不同的类型。这里也将返回值作为一个模板类型即可。
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
很好,并没有发生拷贝。
参考资料
- Modern C++ | 移动语义与完美转发 | Universal Reference
- 泛化之美 –C++11 可变模版参数的妙用
- 移动语义(move semantic)和完美转发(perfect forward)
- 《C++ Primer》