区分地图和集合的模板

Template distinguishing between maps and sets

在为 setunordered_setmapunordered_map 创建代码 common 时,我需要一些方法,其中处理实际上是不同的。我的问题是让编译器推断出要使用哪个实现。

考虑示例:

#include <map>
#include <unordered_set>
#include <string>
#include <iostream>

using namespace std;

static unordered_set<string>    quiet;
static map<const string, const string>  noisy;

template <template <typename ...> class Set, typename K>
static void insert(Set<K> &store, const string &key, const string &)
{
    cout << __PRETTY_FUNCTION__ << "(" << key << ")\n";
    store.insert(key);
}

template <template <typename ...> class Map, typename K, typename V>
static void insert(Map<K, V> &store, const string &key, const string &v)
{
    cout << __PRETTY_FUNCTION__ << "(" << key << ", " << v << ")\n";
    store.insert(make_pair(key, v));
}

int
main(int, char **)
{
    insert(noisy, "cat", "meow");
    insert(quiet, "wallaby", ""); /* macropods have no vocal cords */

    return 0;
}

虽然 cat 行有效,但 wallaby 行会从编译器 (clang-10) 触发以下错误:

t.cc:22:8: error: no matching member function for call to 'insert'
        store.insert(make_pair(key, v));
        ~~~~~~^~~~~~
t.cc:29:2: note: in instantiation of function template specialization
      'insert<unordered_set, std::__1::basic_string<char>, std::__1::hash<std::__1::basic_string<char> > >' requested here
        insert(quiet, "wallaby", ""); /* macropods have no vocal cords */

错误很明显,quiet,它是一个 unordered_set,也被路由到 map 的插入实现——而不是为 unordered_set.

现在,这并非完全没有希望——如果我:

  1. 拼出 所有 模板参数——包括可选参数(比较器、分配器等)
    template <template <typename ...> class Set, typename K, typename A, typename C>
    static void insert(Set<K, A, C> &store, const string &key, const string &)
    ...
    template <template <typename ...> class Map, typename K, typename V, typename A, typename C>
    static void insert(Map<K, V, A, C> &store, const string &key, const string &v)
    
  2. unordered_set 替换为 set

程序将按预期编译和工作 -- 编译器将根据每个模板采用的参数数量(三个对四个)区分 setmap

但是 unordered_set 的参数数量与 map 相同(四个)...而 unordered_map5 个 个参数,所以它不会被路由到地图处理方法...

我如何加强集合处理函数的声明以使其能够处理两种类型的集合? 如何在同一代码中同时处理 mapunordered_map

您可以使用 SFINAE 技术基本上说:仅当 insert 内部调用格式正确时才考虑此重载。例如。像这样:

template <template <typename ...> class Set, typename K>
static auto insert(Set<K> &store, const string &key, const string &)
  -> std::void_t<decltype(store.insert(key))>
{
    cout << __PRETTY_FUNCTION__ << "(" << key << ")" << endl;
    store.insert(key);
}

template <template <typename ...> class Map, typename K, typename V>
static auto insert(Map<K, V> &store, const string &key, const string &v)
  -> std::void_t<decltype(store.insert(make_pair(key, v)))>
{
    cout << __PRETTY_FUNCTION__ << "(" << key << ", " << v << ")" << endl;
    store.insert(make_pair(key, v));
}

Demo

std::mapstd::unordered_map 都有 mapped_type 成员类型,而它们的 set 成员类型没有。因此,我们可以在 std::void_t:

的帮助下添加一些 SFINAE
template<template<typename...> class Map, typename K, typename V,
         typename = std::void_t<typename Map<K, V>::mapped_type>>
void insert(Map<K, V>&, const string&, const string&) {
    // ...
}

一个更通用的解决方案,如果您需要(并且在您的示例中不需要)约束两个函数模板:

template<class, typename = void>
struct is_map : std::false_type { };

template<class Map>
struct is_map<Map, std::void_t<typename Map::mapped_type>> : std::true_type { };

template<template<typename...> class Set, typename K, 
         std::enable_if_t<!is_map<Set<K>>::value, int> = 0>
void insert(Set<K>&, const string&, const string&) {
    // ...
}

template<template <typename...> class Map, typename K, typename V,
         std::enable_if_t<is_map<Map<K, V>>::value, int> = 0>
void insert(Map<K, V>&, const string&, const string&) {
    // ...
}

C++11 解决方案:

template<class...>  // or just <class> if genericity is not needed
struct std_void {
    using type = void;
};

template<template<typename...> class Map, typename K, typename V,
         typename = typename std_void<typename Map<K, V>::mapped_type>::type>
void insert(Map<K, V>&, const string&, const string&) {
    // ...
}

添加注释。问题中的代码针对 GCC 4.4.7。这是一个相当老的 GCC 版本,不完全支持 C++11 标准。特别是,它不支持 using 类型别名,因此 std_void 应该通过老式的 typedef:

实现
template<class...>
struct std_void {
    typedef void type;
};

您必须使用部分模板特化。像这样:

template<template<typename... > class Map, typename T, typename U> static void
insert (Map<T,U>&, const T&, const U&); // generic function
template<typename T, typename U> static void
insert<std::map<T,U>, T, U> (std::map<T,U>&, const T&, const U&); // this is only called if Map is std::map
// you can put more specializations here

第一个函数是一个通用函数,如果类型 Map 没有出现在同一函数的任何模板特化中,则所有 Map 除外 都会调用该函数。第二个函数与第一个函数相同,但仅当 Map 类型为 std::map.

时才会执行函数体

the wikipedia article on partial template specialization

首先,实际的解决方案,适用于编译器版本 当前 在野外找到的 -- 使用 gcc-4.4.7(RedHat6 上的标准编译器)测试, gcc-8.3.1 和 clang-10 是:

template<class...>  // or just <class> if genericity is not needed
struct std_void {
    typedef void type;
};

template<template<typename...> class Map, typename K, typename V,
         typename = std_void<typename Map<K, V>::mapped_type>>
void insert(Map<K, V>& store, const string &key, const string &value) {
    // ...
}

为了(光荣的)未来的 reader,一定要使用@Evg 提出的技术(这个提议基于他的提议)或@Igor-Tandetnik 提出的技术 -- 如果你的编译器支持必要的特性。

Evg 的答案——以及从中得出的这个答案——select 模板基于其类型是否定义了 mapped_type。 Igor 对模板的 insert 方法采用的参数进行了区分。两者都适合我的目的,但 Evg 的更容易适应旧的编译器。


最后,我必须发泄一下我对 C++ 状态的不满:这不应该那么困难。我让它工作了,但一位试图阅读我的代码的同事不会理解它——不是没有多次通过它和无数的诅咒和“陷阱!”

是的,C++ 程序被编译(编译成二进制代码,而不是“字节”代码),完整“反射”所需的 运行 时间类型信息 (RTTI) 是(非常)贵。

但我需要的不需要运行时间反思!所有必要的信息在 编译时 都是已知的,那么为什么我不已经拥有所有必要的语言特性,在它首次被引入三十年后?即使是 10 年等待这样的功能也太长了——芯片制造商在同一时期内使处理器性能提高 32 倍。

constexpr有点像,谢谢您,但是最近才添加,我的编译器仍然不支持它...