最近笔者在使用 C++ 进行编程, 过程中需要对各种实例化的容器进行输出打印, 比如 vector<string>
、vector<int>
, 甚至 vector<Obj>
等自定义类的容器. 对于新的实例化容器, 往往只能采用重写 toString()
函数完成. 最近发现结合 C++ 的 decltype
和 std::function
可以很好的写出一个比较通用的输出函数, 具有很好的复用性. 当然, 或许有其它类似的用途.
- 注: 笔者对 C++ 的掌握和理解并不是很深刻, 若有错误或更好的解决方法也欢迎交流指正
代码
直接上代码:
#include <string>
#include <functional>
using namespace std;
template<typename T>
string vecToString(const vector<T> &vec, const function<string(decltype(vec.front()))> &toStrFunc) {
string str = "[";
size_t sz = vec.size();
for (size_t i = 0; i < sz; ++i) {
str += toStrFunc(vec[i]);
if (i != sz - 1) {
str += ", ";
}
}
return str += "]";
}
该函数完成的即为对一个 vector
类型进行输出, 函数的入口参数有两个, 第一个 vec
当然是需要输出的 vector
本身; 第二个参数 toStrFunc
则为对 vector
容器中的元素进行输出所需要调用的函数.
这里 toStrFunc
是 std::function
类型, 返回值为 string
, 而输入参数是通过 decltype
类型推导出来的, 实际上就是 vec
的元素类型, 即 T
(准确来说是 const T &
). 这里 decltype 推导使用的表达式为 vec.front()
, 该函数返回的为容器的首迭代器的值的引用, 其类型自然为 vec
容器内元素的类型, 因此使用 *vec.begin()
或者 vec[0]
均可满足要求.
通过该函数, 在有对应输出函数的前提下, 即可对该类型的 vector
进行输出.
该方法自然也可以扩展到其它容器, 比如 map
, unordered_map
等等,
使用 decltype 的原因
对于通用字符串函数 vecToString
的实现, 我们容易将其形参 toStrFunc
的类型写为 const function<string(T)> &
, 即使用模板参数 T
而非 decltype 推导. 但这样实际上是错误的, 或者说不能适用于全部情况. 因为对于要输出的容器里的单个元素, 很多情况下不是数字而是像 string
一样的类, 因此往往 toStrFunc
的形参是带有 const &
修饰的, 而将 toStrFunc
类型声明为 const function<string(T)> &
, 实际上将形参的修饰符丢掉了, 因此在元素不是类的情况下, 会匹配失败. 而若将 toStrFunc
类型声明为 const function<string(const T&)> &
, 在实际测试时发现也会出现参数不匹配的错误, 如下图所示. 由于笔者才疏学浅, 这里出错的原因并不是很清楚, 但确实证明了直接用模板参数在 toStrFunc
的类型声明中表示该函数的形参类型是不可行的.
而使用 decltype
类型推导便不会有问题, 无论容器的元素类型是 int
unsigned
等内置的数字类型还是复杂的类乃至是容器, 调用时参数都可以正确匹配.
特别说明
对于上述方法目前有以下几点需要特别说明.
1. 不需要担心容器为空
由于 decltype(exp)
不会执行表达式 exp
, 而是获取类型, 因此即便没有元素 vec[0]
也可以正常使用.
2. 函数模板需要指定实例化类型
由于函数模板在调用时才会实例化, 也就才有函数地址, 因此在作为参数 toStrFunc
传递时有未实例化的情况, 因此需要显示实例化. 比如有一个函数模板 template<T> toStr(T t)
, 在实例化类型为 int
作为的参数传递给 vecToString()
时应该写为 toStr<int>
而不能直接使用 toStr
.
3. 函数重载需要重新封装函数
函数重载导致同名函数有多个, 在作为参数传递时会出现无法确定的情况. 典型的例子就是 to_string()
函数, 该函数将 int
, long
等多种类型转换为字符串, 但实现不是模板而是函数重载, 因此在作为参数时便无法正确解析类型. 比如如下代码在编译时便会报错:
int main() {
vector<int> a{2,3,5};
cout << vecToString(a, to_string);
return 0;
}
解决方法即重新对该函数进行封装, 比如使用模板再次封装一下, 便能正常使用. 代码如下:
template<typename T>
string toStr(T num) {
return to_string(num);
}
int main() {
vector<int> a{2,3,5};
cout << vecToString(a, toStr<int>);
return 0;
}
上述代码输出如下:
4. 与 toStrFunc
函数参数不匹配时需要重新封装函数
此处传递给 toStrFunc
参数的函数类型必须唯一, 即容器内元素的类型, 若有多个参数, 同样需要使用函数进行封装. 当然, 这里也可以使用 std::bind()
函数或者 lambda 函数进行封装.
如以下代码, vec
是一个 vector
的二维数组, 即元素类型为 vector<int>
, 此时不能直接调用 vecToString()
函数, 需要构造一个 vec2str()
的函数作为跳板.
string vec2str(const vector<int>& vec){
return vecToString(vec, toStr<int>);
}
int main() {
vector<vector<int>> vec{{1,2,3}, {2,3,4}};
cout << vecToString(vec, vec2str);
return 0;
}
上述代码输出如下:
同样的, 对于类中的函数的非静态函数也需要重新封装, 由于 function
本身的要求, 对于 Obj::toString() const
在传递时实际上是需要 function<string(const Obj &)>
类型, 因此需要重新进行封装. 代码如下:
class Obj {
private:
int _i;
public:
Obj(int i) : _i(i) {}
string toString() const {
return to_string(_i);
}
// 封装的函数
static string toStr(const Obj& obj) {
return obj.toString();
}
};
int main() {
vector<Obj> vec{{2},{3}};
cout << vecToString(vec, Obj::toStr);
return 0;
}
上述代码输出如下:
5. 可增加 toStrFunc
的默认参数
在大多数情况下, 容器中存放的数据主要以内置的数字类型为主, 因此我们可以对 toStrFunc
设置默认参数, 这样对数字容器调用字符串函数时就只需要传递容器这一个变量了. 代码如下:
template<typename T>
string numToString(T num) {
return to_string(num);
}
template<typename T>
string vecToString(const vector<T> &vec, const function<string(decltype(vec.front()))>& toStrFunc = numToString<T>) {
string str = "[";
size_t sz = vec.size();
for (size_t i = 0; i < sz; ++i) {
str += toStrFunc(vec[i]);
if (i != sz - 1) {
str += ", ";
}
}
return str += "]";
}
int main() {
vector<int> vec{5, 8, 9, 1};
cout << vecToString(vec) << endl;
return 0;
}
上述代码输出如下:
其它例子
当然, 可以将该方法运用到其它容器上, 如下代码是运用到了 unordered_map
上:
template<typename T, typename U>
string mapToString(const unordered_map<T, U>& hashmap, const function<string(decltype(hashmap.begin()->first))> &tFunc,
const function<string(decltype(hashmap.begin()->second))> &uFunc) {
string str = "[";
size_t i = 0, sz = hashmap.size();
for (auto &item: hashmap) {
str += tFunc(item.first) + ":" + uFunc(item.second);
if (++i != sz) {
str += ", ";
}
}
return str += "]";
}
template<typename T>
string toStr(T a) {
return to_string(a);
}
string str(const string& s) {
return s;
}
int main() {
unordered_map<int, string> hashmap{{2,"ff"}, {3, "zzz"}};
cout << mapToString(hashmap, toStr<int>, str);
return 0;
}
上述代码输出如下:
总体而言, 该方法通过将容器内元素的字符串转换函数作为参数来将容器整体转换为字符串, 同时使用模板函数以及 c++11 的 decltype 类型推导来使得字符串转换函数具有了很好的通用性和复用性.