利用 C++11 decltype 和 std::function 构建容器通用字符串函数

最近笔者在使用 C++ 进行编程, 过程中需要对各种实例化的容器进行输出打印, 比如 vector<string>vector<int>, 甚至 vector<Obj> 等自定义类的容器. 对于新的实例化容器, 往往只能采用重写 toString() 函数完成. 最近发现结合 C++ 的 decltypestd::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 容器中的元素进行输出所需要调用的函数.
这里 toStrFuncstd::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 类型推导来使得字符串转换函数具有了很好的通用性和复用性.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值