zoukankan      html  css  js  c++  java
  • C++11改进我们的程序之简化我们的程序1

    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", 20, "shanghai"},{"bb", 25, "beijing"},{"cc", 25, "nanjing"},{"dd", 20, "nanjing"}};
    如果我们按年龄分组的话,得到的结果就是 {20, {"aa", 20, "shanghai"}},{20, {"dd", 20, "nanjing"}},{25,{"bb", 25, "beijing"}},
    {25,{"cc", 25, "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>是编译不过的,这里就要说说如何获取闭包的返回值类型了。
    获取闭包的返回值类型的方法有三种:

    1. 通过decltype
    2. 通过declval
    3. 通过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 Fn> 
    multimap<typename std::result_of<Fn(Arg)>::type, Person> GroupBy(const vector<Person>& vt, const Fn& keySlector)
    {
    typedef typename std::result_of<Fn(Arg)>::type 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;
    }
    
    void TestGroupBy()
    {
    vector<Person> vt{...};
    //按年龄分组
    GroupBy(vt, [](const Person& person){return person.age;});
    //按年龄分组
    GroupBy(vt, [](const Person& person){return person.name;});
    //按姓名分组
    GroupBy(vt, [](const Person& person){return person.city;});
    
    //按姓名和年龄分组
    GroupBy(vt, [](const Person& person){return std::tie(person.name, person.age);});
    }
    复制代码

    恩,一个通用的、简洁的GroupBy函数就这样诞生了,没有重复代码、没有魔法数、没有多余的类型,简洁而优雅,可以坐下喝杯茶再回味一下了...

    c++11 boost技术交流群:296561497,欢迎大家来交流技术。

     
     
  • 相关阅读:
    bootstrap表格内容垂直居中
    [转]配置mysql允许远程连接的方法
    [转]MySQL服务器上添加一个允许远程访问的用户
    [转]Vs解决方案的目录结构设置和管理
    [转]win7下apache2.4响应很慢解决方法
    [转]js中获取时间的函数集
    [转]php和html混编的三种方式
    删除elasticsearch索引脚本
    socket传数据并记录到文件中
    记一次DDos攻击--2016/12/8
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3286220.html
Copyright © 2011-2022 走看看