C++11 根据任意键值分组的优化实现

603 阅读3分钟

「这是我参与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
    });
}