「这是我参与11月更文挑战的第 9 天,活动详情查看:2021最后一次更文挑战」。
参加该活动的第 18 篇文章
预备知识点
std::tie
会将变量的引用整合成一个tuple
,从而实现批量赋值。std::multimap
是关联型容器, 保存<key, value>
键值对, 多个value
可以由相同的key
,multimap
允许按照顺序查找一个子集。std::result_of
用于在编译的时候推导出一个可调用对象(函数, std::funciton 或者重载了 operator() 操作的对象等)的返回值类型.主要用于模板编写中。
问题与方案
假设我们有多个关于 Person
的数据,其中 Person
的属性分为三个,如下所示
struct Person {
std::string name;
int age;
std::string city;
};
std::vector<Person> vt = { {"aa", 20, "shanghai"}, {"bb", 25, "beijing"}, {"cc", 25, "nanjing"}, {"dd", 20, "nanjing"} };
现在如果有一个简单的需求:根据年龄,将 Person 数据进行分组。
比较简单的作法是遍历 vector
中的 Person
,凡是相同年龄的就归为一组,用 multimap<int, Person>
来存放分组。
但是还没完,如果又来一个新的需求要按名字分组呢? 第一反应就是 —— “很简单,只要改下键值为名字就 OK 了” 。
是的,这种办法是可以很简单搞定当前的问题,但是这两种解决方法除了 map 的键值不同外,其它的都一模一样,能简化成一个函数吗(即支持任意的键值)?
直接的想法是通过模板实现,但是直接用模板的话,还有一个问题 —— 键值可能是变化的,它有可能是 Person
中的任意一个字段,也可能是这些字段的任意组合,所以我们无法通过一个简单的泛型 T 去指定键值的类型 !
所以,我们自然而然地想到使用类型擦除技术【可以参考我之前写的一篇文章 【转载】C++ 中的类型擦除】,而本例中将使用 lambda 表达式来实现类型擦除,值得注意的是,其中还涉及了使用 std::result_of
来推断出 lambda 表达式的返回值类型。
具体代码如下:
template <typename R>
class Range {
public:
typedef typename R::value_type value_type; ///< 比如 Person
Range(R& range) : m_range(range) {}
~Range() {}
/// @note 传一个函数对象
/// 以该函数对象的返回值作为键值
/// 最终返回一个存有键值对的 multimap
template <typename Fn>
auto groupby(const Fn& f) ->
/// @note 推测返回值类型
std::multimap<typename std::result_of<Fn(value_type)>::type, value_type> { //decltype(f(*((value_type*)0))),f((value_type&)nullptr)
/// @note 定义推测的键值类型
typedef typename std::result_of<Fn(value_type)>::type ketype;
//typedef decltype(std::declval<Fn>()(std::declval <value_type>())) ketype;
//typedef decltype(f(value_type())) ketype;
/// @note 遍历 m_range (比如 vector 里的 Person ),并构造键值对插入 mymap
std::multimap<ketype, value_type> mymap;
std::for_each(
begin(m_range), end(m_range), [&mymap, &f](value_type item) {
mymap.insert(std::make_pair(f(item), item));
});
return mymap;
}
/// @note 传两个函数对象
template <typename KeyFn, typename ValueFn>
auto groupby(const KeyFn& fnk, const ValueFn& fnv) ->
/// @note 推测返回值类型
std::multimap<typename std::result_of<KeyFn(value_type)>::type, typename std::result_of<ValueFn(value_type)>::type> {
typedef typename std::result_of<KeyFn(value_type)>::type ketype;
typedef typename std::result_of<ValueFn(value_type)>::type valype;
/// @note 遍历 m_range ,并构造键值对插入 mymap
std::multimap<ketype, valype> mymap;
std::for_each(
begin(m_range), end(m_range), [&mymap, &fnk, &fnv](const value_type& item) {
ketype key = fnk(item);
valype val = fnv(item);
mymap.insert(make_pair(key, val));
});
return mymap;
}
private:
R m_range;
};
测试代码:
struct Person {
std::string name;
int age;
std::string city;
};
void TestGroupBy() {
std::vector<Person> vt = { {"aa", 20, "shanghai"}, {"bb", 25, "beijing"}, {"cc", 25, "nanjing"}, {"dd", 20, "nanjing"} };
Range<std::vector<Person>> range(vt);
/// <summary>
/// 指定一个函数对象
/// </summary>
auto r1 = range.groupby([](const Person& person) {
return person.age; ///< 年龄为 key
});
auto r2 = range.groupby([](const Person& person) {
return person.name; ///< 名字为 key
});
auto r3 = range.groupby([](const Person& person) {
return person.city; ///< 城市为 key
});
auto r4 = range.groupby([](const Person& person) {
return std::tie(person.name, person.age); ///< 将 tuple(名字和年龄组成) 作为 key
});
/// <summary>
/// 指定两个函数对象
/// </summary>
auto r5 = range.groupby([](const Person& person) {
return std::tie(person.name, person.age); ///< 将 tuple(名字和年龄组成) 作为 key
},
[](const Person& person) {
return std::tie(person.city); ///< 将 tuple(城市) 作为 value
});
}