c++ 映射和 unordered_map 模板参数:使用 c++20 概念检查常见行为

c++ map and unordered_map template parameter: check for common behavior using c++20 concepts

简而言之:

详细解释:

当从一种编程语言切换到另一种编程语言时,您通常会在下一门语言中错过前一门语言的某些功能。通常这确实意味着一种语言比另一种语言更好(请不要回答或评论开始一种语言 war),但这是语言设计决策和编码人员个人偏好的结果。

特别是,当从 Java 切换到 C++ 时,我怀念 Java 的 classes 的强结构模式。例如,有一个接口 Map 定义了 Map 的预期内容,并且有它的不同实现(HashMap、TreeMap 等),针对某些场景进行了优化。这样,用户可以创建独立于实现的代码(构造容器时除外)。在 C++ 中,有两个常用的地图模板,"map" 和 "unordered_map":

template <typename _Key, typename _Tp, typename _Compare = std::less<_Key>,
        typename _Alloc = std::allocator<std::pair<const _Key, _Tp> > >
    class map
    { ...


template<typename _Key, typename _Tp,
       typename _Hash = hash<_Key>,
       typename _Pred = equal_to<_Key>,
       typename _Alloc = allocator<std::pair<const _Key, _Tp>>>
    class unordered_map
    { ...

如您所见,它们不共享任何公共基础 class,因此,不可能(?)定义接受这两种映射的用户函数。

此外,C++20模板允许“概念”来限制模板参数。如何编写等同于 Java "" 的概念?。似乎有必要将概念重新实现为比所有 class 接口更近的概念 (?)。

我假设是这样说的:

I want restrict some parameter of a template to values with the behavior of a map

你的意思是你想要一个模板 type 参数是一个类似地图的类型。我实际上可以用一个函数原型来回答这个问题的两个部分(参见下面的 doMapStuff)。首先,您需要检查给定类型是“map”还是“类地图”。

检查参数类型

通常,您首先需要某种接口来检查成员是否具有特定的公共成员。但是因为我们只是检查一个类型是否“类似地图”,所以我们可以只检查成员类型。对于类似 map 的类型,一个简单的方法是测试 value_typekey_typemapped_type 成员 types.This 甚至可以轻松完成没有 C++20
从 C++11 开始,我们可以使用 std::is_same<T> 和这样的静态断言:

template<class MapTy>
struct is_map_type : std::is_same<typename MapTy::value_type, std::pair<const typename MapTy::key_type, typename MapTy::mapped_type>> {};

或者,在 C++17 及更高版本中,您甚至可以这样做(使用 C++17 的 std::is_same_v 帮助程序):

template<class MapTy>
constexpr bool is_map_type_v = std::is_same_v<typename MapTy::value_type, std::pair<const typename MapTy::key_type, typename MapTy::mapped_type>>

它们所做的基本上是检查模板类型的 value_type(它存在于许多不同的集合类型中)是否恰好是该类型的 const key_type 和 [=27] 的 std::pair =].这是真的,因此将检查“任何”类地图类型,包括 std::multimap and std::unordered_multimap。 有多种方法可以实现 is_map_type,甚至是 Pre-C++11,其中一些可以在 Whosebug 问题中看到(我上面的代码片段部分取自该问题)。

但是,如果你特别想使用C++20concepts,如果你想以同样的方式实现它,这样做也很简单:

template<class MapTy>
concept is_map_type = std::is_same_v<typename MapTy::value_type, std::pair<const typename MapTy::key_type, typename MapTy::mapped_type>>

或者如果你想使用 C++20 的内置 std::same_as 概念:

template<class MapTy>
concept is_map_type = std::same_as<typename MapTy::value_type, std::pair<const typename MapTy::key_type, typename MapTy::mapped_type>>;

实际功能

it is possible to write a C++20 function that accepts as argument a map, unsorted_map or any other (present or future) implementation of a map ?

if I want restrict some parameter of a template to values with the behavior of a map (map, unsorted_map or any other that appears), which C++20 concept must I use?

实际上,就像我说的,无论是否使用 C++20,您都可以使用一个函数来完成这两项工作。
在 C++11 之后:

template <class MapTy>
void doMapStuff(const MapTy& m) { //function argument doesn't have to be const reference, I'm just doing that in the example
    static_assert(is_map_type<MapTy>::value, "Passed map type isn't a valid map!");
    //Or, if using the C++17 onward version:
    static_assert(is_map_type_v<MapTy>, "Passed map type isn't a valid map!");
    //Do something with argument 'm'...
}

或者,使用 C++20 requires 和我们制作的 C++20 concept

template <class MapTy>
requires is_map_ty<MapTy>
void doMapStuff(const MapTy& m) {
    //...
}

基本上,在任一版本中,该函数都会确保传递的模板参数始终满足我们为类地图类型设置的要求(在本例中检查 value_typekey_type ,和 mappped_type 就像我说的)。否则,程序将无法编译;对于 static_assert 版本,您将在编译期间收到消息字符串和编译错误。对于 C++20 concept 版本,您会收到一个编译错误,指出传递给 doMapStuff 的参数类型不正确。

由于模板类型用作第一个参数的类型,您可以像这样使用函数(无需单独明确指定类型):

std::map<std::string, int> myMap;
doMapStuff(myMap);
//...
std::vector<int> myVec;
doMapStuff(myVec); //compile error! MapTy = std::vector<int>

编辑

根据你在评论中所说的,你似乎只想检查value_type是否是任何一对,这与之前有点不同。我们首先需要一个类型特征来检查模板类型是否是一对。无论 C++ 版本如何,这都几乎相同:

template <typename>
struct is_pair : std::false_type { };

template <typename K, typename V>
struct is_pair<std::pair<K, V>> : std::true_type { };

这将创建一个 bool_cosntant 结构,我们可以使用它来检查传递的类型是否是有效的对。然后我们可以像以前一样定义我们的 is_map 类型特征。
在 C++11 之后:

template<class MapTy>
struct is_map : is_pair<typename MapTy::value_type> {};

//C++17 compliant option
template<class MapTy>
constexpr bool is_map_v = is_pair<typename MapTy::value_type>::value;

在 C++20 中使用 concepts:

template<class MapTy>
concept is_map_like = is_pair<typename MapTy::value_type>::value;

在函数中的用法几乎相同;我们又一次不必显式传递模板类型——它是从传递的参数中推导出来的。
在 C++11 中:

template <class MapTy>
void doMapStuff(const MapTy& m) {
    static_assert(is_map_v<MapTy>, "Passed map type doesn't have a valid pair!");
    //OR...
    static_assert(is_map<MapTy>::value, "Passed map type doesn't have a valid pair!");
}

在 C++20 中使用 concepts:

template <class MapTy>
requires is_map_like<MapTy>
void doMapStuff(const MapTy& m) {
    //...
}

尽管如此,重要的是要注意 mapped_type 别名对于 std“类地图”是唯一的,这与 value_type 不同;原来的 is_map_type 两者都检查。有关详细信息,请参阅

Java 有自己的方式来实现泛型,为此在 C++ 中我们有模板。 因此,您第一个问题的答案是“只需使用模板”。

但重要的是要了解,当您定义模板化函数时,您并不是 定义一个实际的函数,你就是在定义一个编译器可以使用的“配方” 创建一个函数。编译器将创建一个不同的函数(一个模板 实例化,如果你愿意的话)对于每个不同的参数类型 模板。这都是在编译类型中完成的,因此使用模板你会得到 static 多态性.

考虑以下代码

#include <iostream>
#include <map>
#include <string>
#include <unordered_map>

template <typename T>
void print(const T& map) {
    std::cout << "Map:\n";

    for(const auto& [key, value] : map) {
        std::cout << "  " << key << ": " << value << "\n";
    }
}

int main(int argc, char* argv[]) {
    std::map<std::string, double> m;
    std::unordered_map<std::string, double> m2;

    m.insert({"one", 1});
    m.insert({"two", 2});
    m.insert({"three", 3});

    m2.insert({"ten", 10});
    m2.insert({"twenty", 20});
    m2.insert({"thirty", 30});

    print(m);
    print(m2);

    return 0;
}

运行 这个程序产生

Map:
  one: 1
  three: 3
  two: 2
Map:
  thirty: 30
  twenty: 20
  ten: 10

这适用于任何类型,只要传递给 print 的类型可以用 范围为,并且每个“元素”可以使用 structured binding 分解为两个值。 如果您使用一些更特定于地图的方法,例如 insert,则提供的类型 print 函数也必须定义该方法。

现在让我们确认二进制文件中确实有两个不同的函数。假设 生成的二进制名称是“main”,我们可以使用 nm 程序检查生成的二进制文件 在 Linux 中查看在二进制文件中搜索名为“print”的函数。

nm -C main | grep print

我们得到类似

的东西
00000000000033f1 W void print<std::unordered_map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, double, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, double> > > >(std::unordered_map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, double, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, double> > > const&)
00000000000032b3 W void print<std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, double, std::less<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, double> > > >(std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, double, std::less<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, double> > > const&)

输出有点难看,但是我们可以看到我们得到了两个完全独立的函数。 如果我们添加一个 std::unordered_map<std::string, int> 变量并使用 print 函数来 打印它,我们将得到 print 的另一个实现,因为那是不同的类型。

如果我们想打印一个std::vector怎么办?向量支持范围(任何具有 beginend 方法返回迭代器将起作用),但是如果 vector不能用结构化绑定分解成两个值,那就不行了 我们得到一个编译错误。这意味着像 std::vector<std::pair<double, double>> 这样的东西会起作用,但 std::vector<double> 不会。

但是我们的 print 函数在开头打印“Map” 如果它不匹配会更好 std::vector<std::pair<double, double>> 完全没有。这就涉及到你的第二个问题。 模板“太灵活”,可能会导致问题(包括难以理解 错误信息)。有时我们想降低这种灵活性。

为了说明这一点,让我们尝试将 printstd::vector<double> 一起使用。

#include <iostream>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>

template <typename T>
void print(const T& map) {
    std::cout << "Map:\n";

    for(const auto& [key, value] : map) {
        std::cout << "  " << key << ": " << value << "\n";
    }
}

int main(int argc, char* argv[]) {
    std::map<std::string, double> m;
    std::unordered_map<std::string, double> m2;
    std::vector<double> v{1, 2, 3};

    m.insert({"one", 1});
    m.insert({"two", 2});
    m.insert({"three", 3});

    m2.insert({"ten", 10});
    m2.insert({"twenty", 20});
    m2.insert({"thirty", 30});

    print(m);
    print(m2);
    print(v);

    return 0;
}

如果我们尝试编译它,我们会得到类似

的错误
<path>/main.cpp: In instantiation of ‘void print(const T&) [with T = std::vector<double>]’:
<path>/main.cpp:47:10:   required from here
<path>/main.cpp:11:21: error: cannot decompose non-array non-class type ‘const double’
   11 |     for(const auto& [key, value] : map) {
      |                     ^~~~~~~~~~~~

我们可以为std::vector<T>定义一个单独的print函数。例如,运行 下面的代码

#include <iostream>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>

template <typename T>
void print(const T& map) {
    std::cout << "Map:\n";

    for(const auto& [key, value] : map) {
        std::cout << "  " << key << ": " << value << "\n";
    }
}

template <typename T>
void print(const std::vector<T>& v) {
    std::cout << "v: [";
    for (const auto& elem : v) {
        std::cout << elem << ", ";
    }
    std::cout << "]\n";
}

int main(int argc, char* argv[]) {
    std::map<std::string, double> m;
    std::unordered_map<std::string, double> m2;
    std::vector<double> v{1, 2, 3};

    m.insert({"one", 1});
    m.insert({"two", 2});
    m.insert({"three", 3});

    m2.insert({"ten", 10});
    m2.insert({"twenty", 20});
    m2.insert({"thirty", 30});

    print(m);
    print(m2);
    print(v);

    return 0;
}

结果

Map:
  one: 1
  three: 3
  two: 2
Map:
  thirty: 30
  twenty: 20
  ten: 10
v: [1, 2, 3, ]

这很好,但是如果我们希望我们的 print 向量函数可以处理任何东西怎么办? 表现得像一个向量?如果我们只使用 void print(const T& v) 作为我们的“类向量” print 函数由于 print 的重新定义而出现编译错误。我们必须限制 每个 print 函数与不相交的“类型集”一起工作,每个都遵守一些条件。

在 c++20 之前,您的选择是使用 type traits 和静态断言(之前 C++17) 或 if constexpr。使用 C++20,您可以使用 concepts.

获得更好(更简单)的方法

Arastais 的回答涵盖了这一点,我将添加一些评论。要求存在 value_typekey_type 完全没问题,任何第三方“类地图” class 都是 鼓励实现这些别名作为使用通用代码(您的模板)的一种方式 是在考虑到 STL 容器的情况下创建的。这就是为什么 STL 容器有 这些别名1 在第一手,使它更容易编写泛型代码。但它是 可能某些第三种地图类型没有这些别名,但仍然有 “类地图”界面2您对“类地图”类型的使用 而言。 在那种情况下,您可能会考虑使用实际使用的成员函数的存在作为 接受模板的条件。这就是 C++20 概念真正大放异彩的地方。它是 结合现有概念或使用 requires 来定义此类概念真的很容易 表达式.

脚注

1 stl类型见cppreference中的“成员类型”部分,如vectorunordered_map, set,等等

2 也许你只需要 insert 方法,或者使用 lime 访问一个值 mymap[key],例如。如果这足够了,您可以将其用作您的“地图界面” 定义条件。