(原创)C++11改进我们的程序之简化我们的程序(一)
C++11在很多方面可以简化我们的程序开发,我会在“简化我们的程序”这一系列的博文中一一讲到,敬请关注。这次要讲的是:C++11如何通过获取函数模板的返回值类型来简化我们的程序。
在谈到简化之前,我们先看一个问题,这个问题也是我前段时间在开发C++版本的linq时遇到的。假设我们现在需要将集合按某种属性分组,就是类似于sql语句中的group by,我们知道group by后面的字段会组成一个唯一的键,得到的结果是按照这个唯一键值的分组。关于group by具体看一个例子就清楚了。
struct Person
{
string name;
int age;
string city;
}; vector<Person> vt = {{"aa", , "shanghai"},{"bb", , "beijing"},{"cc", , "nanjing"},{"dd", , "nanjing"}};
如果我们按年龄分组的话,得到的结果就是 {, {"aa", , "shanghai"}},{, {"dd", , "nanjing"}},{,{"bb", , "beijing"}},
{,{"cc", , "nanjing"}},实际上最终结果就是两组,第一组是键为20,值是名称为"aa"和"dd"的两个Person;
第二组是键为25,值是名称为"bb"和"cc"的两个Person。
通过这个例子,大家应该对group by的含义就清楚了,做起来也好做。
比较简单的作法是遍历vector中的Person,凡是相同年龄的就归为一组,用multimap<int, Person>来存放分组,代码可能是这样的:
multimap<int, Person> GroupByAge(const vector<Person>& vt)
{
multimap<int, Person> map;
std::for_each(vt.begin(), vt.end(), [&map](const Person& person)
{
map.insert(make_pair(person.age, person));
}); return map;
}
写完上面的代码后再测试下发现没问题,很简单就搞定了。但还没完,如果我要按名称分组呢?第一反应就是, 不是一样的吗,很简单,copy一下改下键值就OK了。
multimap<string, Person> GroupByName(const vector<Person>& vt)
{
multimap<string, Person> map;
std::for_each(vt.begin(), vt.end(), [&map](const Person& person)
{
map.insert(make_pair(person.name, person));
}); return map;
}
是的,很简单就搞定了,但是大家有没有发现这两段代码除了map的键值不同外,其它的都一模一样,能简化成一个函数吗?能,通过模板搞定嘛。
template<typename T>
multimap<T, Person> GroupBy(const vector<Person>& vt)
{
multimap<T, Person> map;
std::for_each(vt.begin(), vt.end(), [&map](const Person& person)
{
map.insert(make_pair(person.name, person)); //不行了,这个地方不能选择键值了
}); return map;
}
当我们写下上面的代码时发现行不通了,因为map.insert(make_pair(person.name, person)); 这里的键值可能是变化的,它有可能是Person中的任意一个字段,
也可能是这些字段的任意组合。问题就就在这里,我们不能通过一个泛型的模板参数T去选择键值的类型!这样的话以后如果要根据城市分组的话是不是又要拷贝
一份代码,而仅仅是改个键值。这种蛋疼的重复代码很丑陋,重复代码是万恶之源,一定要消除!如何消除这种重复呢?我们先分析一下这几段重复代码的特征:仅仅是某个类型不同,其它行为是一样的,
问题的关键就是如何把这些不同的类型统一起来!本质上就是如何将类型擦除,关于类型擦除,我前面的博文讲到过,不知道的童鞋请点这里,c++中的类型擦除,
里面介绍了五种方式,在这里我打算用第五种方式,通过闭包去擦除类型,因为键值的选择权在外面,应该开放给用户去选择。代码可能是这样:
template<typename T>
multimap<T, Person> GroupBy(const vector<Person>& vt, const Fn& keySlector)
{
multimap<T, Person> map;
std::for_each(vt.begin(), vt.end(), [&map](const Person& person)
{
map.insert(make_pair(keySlector(person), person)); //keySlector返回值就是键值,通过keySelector擦除了类型
}); return map;
}
上面的代码通过闭包来擦除了键值类型,至于到底是什么类型的键值,都是keySlector中决定,是age还是name或者是city都OK。
测试代码:
void TestGroupBy()
{
vector<Person> vt{...};
//按年龄分组
GroupBy<int>(vt, [](const Person& person){return person.age;});
//按年龄分组
GroupBy<string>(vt, [](const Person& person){return person.name;});
//按年龄分组
GroupBy<string>(vt, [](const Person& person){return person.city;});
}
恩,终于通过类型擦除把逻辑都统一成一个函数了,简化了N多重复代码,看得也挺舒服的。不过没完,是的,就是没完,睁大眼睛再看看吧。
对于这段代码,至于你们满不满意我不知道,我不满意。我不满意的地方有一个就是GroupBy<T>要带一个类型,这个类型是键值类型,也是闭包keySelector
返回值的类型。为什么每次都要把这个类型带上呢,我很懒,不想把它带上,我觉得通过keySelector就可以推断出返回值类型,完全没必要带着这个类型,除了
多敲几个代码之外没有任何好处。是的,我就想这样调用:
GroupBy(vt, [](const Person& person){return person.age;}); //不需要带着闭包的返回值类型
GroupBy(vt, [](const Person& person){return person.name;});
GroupBy(vt, [](const Person& person){return person.city;});
要做到这样,问题的关键是如何推断出lamda表达式的返回值类型!C++11不是提供了推断表达式类型decltype吗,试试看如何推断出lamda表达式的返回值类型吧。
template<typename Fn>
multimap<T, Person> GroupBy(const vector<Person>& vt, const Fn& keySlector)
{
typedef decltype(keySlector(Person)) key_type; //推断出keySlector的返回值类型
multimap<key_type, Person> map;
std::for_each(vt.begin(), vt.end(), [&map](const Person& person)
{
map.insert(make_pair(keySlector(person), person));
}); return map;
}
问题是函数返回值中的那个T如何搞定呢?这样multimap<decltype(keySlector(Person)), Person>是编译不过的,这里就要说说如何获取闭包的返回值类型了。
获取闭包的返回值类型的方法有三种:
- 通过decltype
 - 通过declval
 - 通过result_of
 
我们先看看第一种方式,通过decltype:
multimap<decltype(keySlector((Person&)nulltype)), Person>或者multimap<decltype(keySlector(*((Person*)0))), Person>
这种方式可以解决问题,但不够好,因为它有两个魔法数:nulltype和0,看得蛋疼,不知道它们是怎么跑出来的。再看看第二种方式吧:
通过declval:
multimap<decltype(declval(Fn)(declval(Person))), Person>
这种方式用到了declval,declval的强大之处在于它能获取任何类型的右值引用,而不管它是不是有默认构造函数,因此我们通过declval(Fn)获得了function的右值引用,
然后再调用形参declval(Person)的右值引用,需要注意的是declval获取的右值引用不能用于求值,因此我们需要用decltype来推断出最终的返回值。
这种方式比刚才那种方式要好一点,因为消除了魔法数,但是感觉稍微有点麻烦,写的代码有点繁琐,有更好的方式吗?看第三种方式吧:
通过result_of
multimap<typename std::result_of<Fn(Person)>::type, Person>
std::result_of<Fn(Arg)>::type可以获取function的返回值,没有魔法数,也没有declval繁琐的写法,很优雅。其实,查看源码就知道result_of内部就是通过declval实现的,作法和方式二一样,只是简化了写法。
至此,我们解决了所有问题,看看最终的GroupBy函数吧:
template<typename R>
class Range
{
public:
typedef typename R::value_type value_type;
Range(R& range) : m_range(range)
{
}
~Range()
	{
	}
template<typename Fn>
	multimap<typename std::result_of<Fn(value_type)>::type, value_type> groupby(const Fn& f)  //decltype(f(*((value_type*)0))),f((value_type&)nullptr)
	{
		/
/typedef typename std::result_of<Fn(value_type)>::type ketype;
		typedef  decltype(std::declval<Fn>()(std::declval <value_type>())) ketype;
		//ty
pedef decltype(f(value_type())) ketype;
		multimap<ketype, value_type> mymap;
		std::for_each(begin(m_range), end(m_range), [&mymap, &f](value_type item)
		{
			mymap.insert(make_pair(f(item), item));
		});
		return mymap;
	}
template<typename KeyFn, typename ValueFn>
	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;
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;
};
有童鞋反应之前的代码不通用,我只是举个例子,做到通用比较简单,上面的range中的grouby对任何集合都有效。测试代码:
struct Person
{
string name;
int age;
string city;
}; template<typename T, typename... R>
void PrintMap(T t, const R&... range)
{
PrintMap(t);
PrintMap(range...);
} template<typename R>
void PrintMap(const R& range)
{
for (auto it = std::begin(range); it != std::end(range); it++)
{
std::cout << it->first << " " << it->second.name << " " << it->second.age << " " << it->second.city << endl;
}
} template<class Tuple, std::size_t N>
struct TuplePrinter {
static void print(const Tuple& t)
{
TuplePrinter<Tuple, N - >::print(t);
std::cout << ", " << std::get<N - >(t);
}
}; template<class Tuple>
struct TuplePrinter<Tuple, >{
static void print(const Tuple& t)
{
std::cout << std::get<>(t);
}
}; template<class... Args>
void PrintTuple(const std::tuple<Args...>& t)
{
std::cout << "(";
TuplePrinter<decltype(t), sizeof...(Args)>::print(t);
std::cout << ")\n";
} void TestGroupBy()
{
vector<Person> vt = { {"aa", , "shanghai"}, { "bb", , "beijing" }, { "cc", , "nanjing" }, { "dd", , "nanjing" } };
Range <vector<Person>> range(vt);
auto r1 = range.groupby([](const Person& person){return person.age; });
auto r2 = range.groupby([](const Person& person){return person.name; });
auto r3 = range.groupby([](const Person& person){return person.city; });
auto r4 = range.groupby([](const Person& person){return std::tie(person.name, person.age); });
auto r5 = range.groupby([](const Person& person){return std::tie(person.name, person.age); }, [](const Person& person){return std::tie(person.city); }); PrintMap(r1, r2, r3);
for (auto it = std::begin(r5); it != std::end(r5); it++)
{
PrintTuple(it->first);
PrintTuple(it->second);
}
}
测试结果:

恩,一个通用的、简洁的GroupBy函数就这样诞生了,没有重复代码、没有魔法数、没有多余的类型,简洁而优雅,可以坐下喝杯茶再回味一下了...
c++11 boost技术交流群:296561497,欢迎大家来交流技术。
(原创)C++11改进我们的程序之简化我们的程序(一)的更多相关文章
- (原创)C++11改进我们的程序之简化我们的程序(八)
		
本次要讲的是如何通过泛型函数来简化我们的程序. 泛型函数除了之前介绍的一些优点外还有两个重要的优点 1.消除重复逻辑,提高程序的内聚性和健壮性 泛型函数在某种程度上用来弥补泛型类型的不足.通过泛型类型 ...
 - (原创)C++11改进我们的程序之简化我们的程序(二)
		
这次要讲的是:C++11如何通过组合函数来简化我们的程序.关于组合函数,大家可能对这个概念有点陌生.组合函数是将N个一元函数组成一种更复杂的函数,每个函数的返回值作为参数传给下一个函数,直到传到最后一 ...
 - C++11改进我们的程序之简化我们的程序1
		
C++11改进我们的程序之简化我们的程序(一) C++11在很多方面可以简化我们的程序开发,我会在“简化我们的程序”这一系列的博文中一一讲到,敬请关注.这次要讲的是:C++11如何通过获取函数模板的返 ...
 - (原创)C++11改进我们的程序之简化我们的程序(四)
		
这次要讲的是:c++11统一初始化.统一begin()/end()和for-loop循环如何简化我们的程序 初始化列表 c++11之前有各种各样的初始化语法,有时候初始化的时候还挺麻烦,比较典型的如v ...
 - (原创)C++11改进我们的程序之简化我们的程序(三)
		
这次要讲的是:C++11如何通过auto.decltype和返回值后置来简化我们的程序. auto和c#中的var类似,都是在初始化时自动推断出数据类型.当某个变量的返回值难于书写时,或者不太确定返回 ...
 - (原创)c++11改进我们的模式之改进代理模式,实现通用的AOP框架
		
c++11 boost技术交流群:296561497,欢迎大家来交流技术. 本次要讲的时候如何改进代理模式,具体来说是动态代理模式,动态代理模式一般实现AOP框架,不懂AOP的童鞋看这里.我前面的博文 ...
 - (原创)c++11改进我们的模式之改进命令模式
		
模式虽然精妙,却难完美,比如观察者模式中观察者生命周期的问题:比如访问者模式中循环依赖的问题等等:其它很多模式也存在这样那样的一些不足之处,如使用场景受限.实现复杂.不够简洁.不够通用等.但我觉得不足 ...
 - (原创)c++11改进我们的模式之改进访问者模式
		
本次讲c++11改进我们的模式之改进访问者模式 访问者模式是GOF23个设计模式中比较复杂的模式之一,但是它的功能也很强大,非常适合稳定的继承层次中对象的访问,可以在不修改被访问对象的情况下,动态添加 ...
 - (原创)C++11改进我们的程序之简化我们的程序(七)
		
这次要讲的内容是:c++11中的tuple(元组).tuple看似简单,其实它是简约而不简单,可以说它是c++11中一个既简单又复杂的东东,关于它简单的一面是它很容易使用,复杂的一面是它内部隐藏了太多 ...
 
随机推荐
- 跟我学Shiro---无状态 Web 应用集成
			
无状态 Web 应用集成 在一些环境中,可能需要把 Web 应用做成无状态的,即服务器端无状态,就是说服务器端不会存储像会话这种东西,而是每次请求时带上相应的用户名进行登录.如一些 REST 风格的 ...
 - js template实现方法
			
<script type="text/html" id="template"> <li class="list-item" ...
 - 初次使用Microsoft Azure
			
一.介绍 在微博上偶然发现微软的Azure有免费申请试用的机会,于是赶快给微软发邮件申请,第二天就通过了. 早就听说过微软在云计算方面发力,但一直没机会试用,之前用过国产的BAE.SAE,用GoAge ...
 - 【onethink1.0】HTML模板获取前台和后台当前登录用户名
			
1.版本:onethink1.0:位置:HTML模板 前台:{:get_username()} 后台:{:session('user_auth.username')}
 - android中的目录结构介绍
			
Google Android手机的软件为了安全性和稳定性都是默认安装到手机内存里,但是手机内存有限,所以我们会做app2sd操作,来让我们安装的软件放到sd卡上,这个操作是需要rom的支持的. ...
 - Dubbo OPS工具——dubbo-admin & dubbo-monitor
			
1. 前言 今年八月份的时候,查看github仓库,Dubbo OPS还提供了三种工具用于Dubbo的监控运维: 九月份,伴随着Dubbo的全面快速的升级,现在仓库里Dubbo OPS下这三个工具已经 ...
 - grep 和 awk的buffer
			
当使用 tail -f test.log | grep "mode" | awk '{print $5}'命令 或者 tail -f test.log | awk '/mode/ ...
 - sikuli 如何 清空文本框中的内容??解决方法!
			
Screen s = new Screen(); s.click("name.png"); s.type("a",KeyModifier.CTRL); s.ty ...
 - 消息队列状态:struct msqid_ds
			
Linux的消息队列(queue)实质上是一个链表, 它有消息队列标识符(queue ID). msgget创建一个新队列或打开一个存在的队列; msgsnd向队列末端添加一条新消息; msgrcv从 ...
 - Git的图形化工具使用教程
			
虽然感觉并没有什么暖用,但姑且还是写出来留作纪念好了 Git这种分布式版本控制系统最适合的就是单枪匹马搞开发的选手,不需要服务器,下载个git和图形工具,网速快十分钟就能搞定开始愉快的开发工作.我在搭 ...