Skip to content

Latest commit

 

History

History
294 lines (224 loc) · 12 KB

Chapter15.md

File metadata and controls

294 lines (224 loc) · 12 KB

Chapter15. Modern C++ 标准库容器

本章你将看到现代C++中新增的容器介绍。

15.1 span

假设使用这个函数打印一个vector中的内容:

void print(const vector<int>& values) {
  for (const auto& value: values) { cout << value << " "; }
  cout << endl;
}

假设还想打印C风格数组的内容,一种选择是重载 print() 函数,以接受指向数组第一个元素的指针,以及要打印的元素数量:

void print(const int values[], size_t count) {
  for (size_t i { 0 }; i < count; i++) { cout << values[i] << " "; }
  cout << endl;
}

如果还想打印 std::array,那么可以提供第三个重载,但是函数的参数类型是什么?对于 std::array,必须指定array中的元素类型和数量作为模板参数。事情似乎变得越来越复杂了。

std::span 在C++20中引入并在 <span> 中定义,它允许编写单个函数来处理vector、C风格数组和任意大小的 std::array。下面是使用span的print函数实现:

void print(span<int> values) {
  for (const auto& value : values) { cout << value << " "; }
  cout << endl;
}

注意,span的复制成本很低。它基本上只包含一个指向序列第一个元素的指针和一些元素。span永远不会复制数据。因此,它通常是通过值传递的。

有几个用于创建span的构造函数。例如,可以创建一个包含给定vector、std::array或C风格数组的所有元素的数组。还可以通过传递第一个元素的地址和想要在span中包含的元素的数量,来创建一个只包含部分容器元素的span。

可以使用 subspan() 方法从现有的span创建子视图。它的第一个参数是span中的偏移量,第二个参数是包含在子视图中的元素数量。还有两个名为 first()last() 的附加方法,分别返回前n个元素或后n个元素的span子视图。

span有类似于vector和array的方法:begin() end() rbegin() rend() front() back() operator[] data() size()empty()

下面的代码演示了调用 print(span) 的几种方法:

vector v {11, 22, 33, 44, 55, 66};
// pass the whole vector, implicitly converted to a span
print(v);
// pass an explicitly created span
span mySpan{v};
print(mySpan);
// create a subview and pass that
span subspan{mySpan.subspan(2, 3)};
print(subspan);
// pass a subview created in-line
print({v.data() + 2, 3});

// pass an std::array
array<int, 5> arr {5, 4, 3, 2, 1};
print(arr);
print({arr.data() + 2, 3});

// pass a C-style array
int carr[] {9, 8, 7, 6, 5};
print(carr);
print({carr + 2, 3}); // a subview of the C-style array

output

11 22 33 44 55 66
11 22 33 44 55 66
33 44 55
33 44 55
5 4 3 2 1
3 2 1
9 8 7 6 5
7 6 5

与提供字符串只读视图的 string_view 不同,span可以提供对底层元素的读写访问。记住,span值包含一个指向序列中第一个元素的指针和元素的数量;也就是说,span永远不会复制数据。因此,修改span中的元素实际上就是修改底层序列中的元素。如果不需要,可以创建一个包含const元素的span。例如,print() 函数没有理由修改指定span中的任何元素。可以通过以下方式防止修改:

void print(span<const int> values) {
  for (const auto& value : values) { cout << value << " "; }
  cout << endl;
}

注意

在编写接受 const vector<T>& 的函数时,考虑使用 span<const T> 作为替换。这样函数就可以处理来自vector, array, C风格数组等的数据序列的视图和子视图。

15.2 哈希函数

哈希函数的结果未必是唯一的。两个或多个键哈希到同一个桶索引,就称为冲突(collision) 。当使用不同键得相同的哈希值,或把不同的哈希值转换为同一个桶索引时,就会发生冲突。可采用多种方法处理冲突,例如 二次重哈希(quadratic re-hashing)线性链(linear chaining) 等方法。

哈希函数的选择非常重要。不产生冲突的哈希函数称为“完美哈希”。完美哈希的查找时间是常量;常规的哈希查找时间接近1,与怨怒是数量无关。随着冲突的增加,查找时间会增加,性能会降低。

C++标准为所有基本数据类型提供了哈希函数,还为error_code, error_condition, optional, variant, bitset, unique_ptr, shared_ptr, type_index, string, string_view, vector<bool>, thread::id提供了哈希函数。如果使用的键类型没有可用的标准哈希函数,就必须实现自己的哈希函数。

下面演示如何编写自定义的哈希函数。代码定义了一个类IntWrapper,它仅封装了一个整数。还提供了 operator==,因为这是在无效关联容器中使用键所必须的。

class IntWrapper {
public:
  IntWrapper(int i) : m_wrapperInt { i } {}
  int getValue() const { return m_wrapperInt; }
  bool operator==(const IntWrapper& other) const = default; // =default since C++20
private:
  int m_wrapperInt;
};

为给IntWrapper类编写哈希函数,应该先给IntWrapper编写 std::hash 模板的特例。 std::hash 模板在 <functional> 头文件中定义。这个特例需要实现函数调用运算符,计算并返回给定IntWrapper实例的哈希值。对于本例,请求被简单地转发给了整数的标准哈希函数:

namespace std {
template<> struct hash<IntWrapper> {
  size_t operator()(const IntWrapper& wrapper) const {
    return std::hash<int>{}(wrapper.getValue());
  }
};
}

注意,一般不允许把任何内容放在std命名空间中,但std类模板的特例是这条规则的例外。如果类包含多个数据成员,就需要在计算哈希时考虑所有数据成员,这不是这里要讨论的内容。

15.3 unordered_map

unordered_map容器在 <unordered_map> 头文件中定义,是一个类模板,如下所示:

template <class Key,
          class T,
          class Hash = hash<Key>,
          class Pred = std::equal_to<Key>,
          class Alloc = std::allocator<std::pair<const Key, T>>>
class unordered_map;

总共有5个模板参数:键类型、值类型、哈希类型、判等比较类型和分配器类型。

与普通的map一样,unordered_map中的所有键都应该是唯一的。除此之外,它有一些哈希专用方法。例如,load_factor() 返回每一个桶的平均元素数,以反映冲突的次数。bucket_count() 方法返回容器的桶的数量。还提供了local_iterator和const_local_iterator,用于遍历单个桶中的元素,但是不能用来遍历多个桶。bucket(key) 方法返回指定元素的桶的索引,begin(n) 返回引用索引为n的桶中第一个元素的local_iterator。end(n) 返回引用索引为n的桶中最后一个元素之后的local_iterator。

unordered_map 示例:电话簿

下面的示例将使用 unordered_map 表示电话簿。使用人名表示键,电话号码则是与键相关的值。

void printMap(const auto& m) {    // C++20 abbreviated function template
  for (const auto& [key, value] : m) {
    std::cout << std::format("{} (Phone: {})", key, value) << '\n';
  }
  std::cout << "-------" << '\n';
}

int main() {
  // create a hash table
  std::unordered_map<std::string, std::string> phoneBook {
    {"Marc G.", "123-456789"},
    { "Scott K.", "654-987321" } };
  printMap(phoneBook);

  // add/remove some phone numbers
  phoneBook.insert(std::make_pair("John D.", "321-987654"));
  phoneBook["Johan G."] = "963-258147";
  phoneBook["Freddy K."] = "999-256256";
  phoneBook.erase("Freddy K.");
  printMap(phoneBook);

  // find the bucket index for a specific key
  const size_t bucket{ phoneBook.bucket("Marc G.") };
  std::cout << std::format("Marc G. is in bucket {} containing the following {} names:",
    bucket, phoneBook.bucket_size(bucket)) << '\n';
  // get begin and end iterators for the elements in this bucket
  // "auto" is here. The complier deduces the type of
  // both as unordered_map<string, string>::const_local_iterator
  auto localBegin{ phoneBook.cbegin(bucket) };
  auto localEnd{ phoneBook.cend(bucket) };
  for (auto iter{ localBegin }; iter != localEnd; ++iter) {
    std::cout << std::format("\t{} (Phone: {})", iter->first, iter->second) << '\n';
  }
  std::cout << "-------" << '\n';

  // print some statistics about the hash table
  std::cout << std::format("There are {} buckets.", phoneBook.bucket_count()) << '\n';
  std::cout << std::format("Average number of elements in a bucket is {}.",
    phoneBook.load_factor()) << '\n';
}

output

Scott K. (Phone: 654-987321)
Marc G. (Phone: 123-456789)
-------
Scott K. (Phone: 654-987321)
Marc G. (Phone: 123-456789)
Johan G. (Phone: 963-258147)
John D. (Phone: 321-987654)
-------
Marc G. is in bucket 1 containing the following 2 names:
        Scott K. (Phone: 654-987321)
        Marc G. (Phone: 123-456789)
-------
There are 8 buckets.
Average number of elements in a bucket is 0.5.

15.4 unordered_multimap

unordered_map是允许多个元素带有同一个键的unordered_map。两者的接口几乎相同,区别在于:

  • unordered_multimap没有提供 operator[] 运算符和 at()
  • 在unordered_multimap上执行插入操作总会成功。因此,添加单个元素的 unordered_map::insert() 方法只返回迭代器而非pair。
  • unordered_map支持 insert_or_assign()try_emplace() 方法,但是,unordered_multimap不支持这两个方法。

15.5 unordered_set unordered_multiset

<unordered_set> 头文件定义了unordered_set和unordered_multiset,这两者分别类似于set和multiset。区别在于它们不会对键进行排序,而且使用了哈希函数。

15.6 bitset

bitset是固定长度的位序列的抽象。bitset还使用了设置(set)和清零(unset)两个术语。可将一个位从一个值切换(toggle)或翻转(flip)为另一个值。

bitset并不是真正的标准库容器:bitset的大小固定,没有对元素类型进行模板化,也不支持迭代器。然而,这是一个有用的工具类,而且常和容器在一起,因此这里做一下简要介绍。

15.6.1 bitset基础

bitset定义在 <bitset> 头文件中,根据保存的位数进行模板化。默认构造函数将bitset的所有字段初始化为0。另一个构造函数根据0和1字符组成的字符串创建bitset。

可通过 set() reset() flip() 方法改变单个位的值,通过重载的 operator[] 可以访问和设置单个字段的值。注意,对非const对象应用 operator[] 会返回一个代理对象,可为这个代理对象赋予一个布尔值,调用 flip()~ 取反。还可以通过 test() 方法访问单个字段。bitset以包含0或1字符的字符串形式进行流式处理。此外,通过普通的插入和抽取运算符可以流式处理bitset。bitset作为包含0和1字符的字符串进行流处理。

下面是一个简单的示例:

bitset<10> myBitset;

myBitset.set(3);
myBitset.set(6);
myBitset[8] = true;
myBitset[9] = myBitset[3];

if (myBitset.test(3)) {
  cout << "Bit 3 is set!" << endl;
}
cout << myBitset << endl;

output

Bit 3 is set!
1101001000

15.6.2 按位运算符

除基本的位操作外,bitset还实现了所有按位元素符。示例如下:

auto str1 {"0011001100"};
auto str2 {"0000111100"};
bitset<10> bitsOne { str1 };
bitset<10> bitsTwo { str2 };

auto bitsThree { bitsOne & bitsTwo };
cout << bitsThree << endl;
bitsThree <<= 4;
cout << bitsThree << endl;

output

0000001100
0011000000