现代c++编程c++11/14/17/20:Chapter 04 Containers

4.1 Linear Container

4.1.1 std::array

当你看到这个容器时,你肯定会遇到这样的问题:

  1. 为什么直接介绍std::array而不是std::vector ?
  2. 已经有传统数组了,为什么要用std::array?

首先回答第一个问题。与std::vector不同,std::array对象的大小是固定的。如果容器的大小是固定的,那么可以先使用std::array容器。此外,由于std::vector是自动展开的,当存储大量数据时,容器被删除,容器不会自动返回被删除元素的对应内存。在本例中,您需要手动运行shrink_to_fit()来释放这部分内存。

std::vector<int> v;
std::cout << "size:" << v.size() << std::endl; // output 0
std::cout << "capacity:" << v.capacity() << std::endl; // output 0
`// As you can see, the storage of std::vector is automatically managed and
// automatically expanded as needed.
// But if there is not enough space, you need to redistribute more memory,
// and reallocating memory is usually a performance-intensive operation.
v.push_back(1);
v.push_back(2);
v.push_back(3);
std::cout << "size:" << v.size() << std::endl; // output 3
std::cout << "capacity:" << v.capacity() << std::endl; // output 4

// The auto-expansion logic here is very similar to Golang's slice.
v.push_back(4);
v.push_back(5);
std::cout << "size:" << v.size() << std::endl; // output 5
std::cout << "capacity:" << v.capacity() << std::endl; // output 8

// As can be seen below, although the container empties the element,
// the memory of the emptied element is not returned.
v.clear();
std::cout << "size:" << v.size() << std::endl; // output 0
std::cout << "capacity:" << v.capacity() << std::endl; // output 8

// Additional memory can be returned to the system via the shrink_to_fit() call
v.shrink_to_fit();
std::cout << "size:" << v.size() << std::endl; // output 0
std::cout << "capacity:" << v.capacity() << std::endl; // output 0

第二个问题要简单得多。使用std::array可以使代码更现代,并封装一些操作函数,如获取数组大小和检查它是否为空,以及使用标准友好型。标准库中的容器算法,如std::sort。

使用std::array就像指定它的类型和大小一样简单:
std::array<int, 4> arr = {1, 2, 3, 4};

arr.empty(); // check if container is empty
arr.size(); // return the size of the container

	// iterator support
	for (auto &i : arr)
	{
		// ...
	}
	// use lambda expression for sort
	std::sort(arr.begin(), arr.end(), [](int a, int b) {
		return b < a;
	});
// array size must be constexpr
constexpr int len = 4;
std::array<int, len> arr = {1, 2, 3, 4};


// illegal, different than C-style array, std::array will not deduce to T*
// int *arr_p = arr;

当我们开始使用std::array时,不可避免地会遇到c风格兼容的接口。有三种方法可以做到这一点:

void foo(int *p, int len) {
return;
}
std::array<int, 4> arr = {1,2,3,4};
// C-stype parameter passing
// foo(arr, arr.size()); // illegal, cannot convert implicitly
foo(&arr[0], arr.size());
foo(arr.data(), arr.size());
// use `std::sort`
std::sort(arr.begin(), arr.end());

4.1.2 std::forward_list

std::forward_list是一个列表容器,它的用法基本上类似于std::list,所以我们不用花很多时间介绍它。

需要知道的是,与std::list的双链表的实现不同,std::forward_list是使用单链表实现的。提供O(1)复杂度的元素插入,不支持快速随机访问(这也是链表的一个特性),它也是标准库容器中唯一不提供size()方法的容器。在不需要双向迭代时,具有比std::list更高的空间利用率。

4.2 Unordered Container

我们已经熟悉传统c++中的有序容器std::map/std::set。这些元素由红黑树在内部实现。插入和搜索的平均复杂度是O(log(size))。在插入元素时,根据<操作符比较元素的大小,并确定元素是相同的。并选择合适的位置插入容器。在遍历容器中的元素时,输出将按照<操作符的顺序逐个遍历。

无序容器中的元素不是排序的,其内部由哈希表实现。插入和搜索元素的平均复杂度为O(常数),可以在不考虑容器内元素顺序的情况下获得显著的性能提升。

c++ 11引入了两组无序容器:std::unordered_map/std::unordered_multimap和std::unordered_set/std::unordered_multiset。

它们的用法基本类似于原来的std::map/std::multimap/std::set/set::multiset
由于我们对这些容器已经很熟悉了,所以我们将不逐一比较它们。让我们直接比较std::map和std::unordered_map:

#include <iostream>
#include <string>
#include <unordered_map>
#include <map>
int main() {
	// initialized in same order
	std::unordered_map<int, std::string> u = {
		{1, "1"},
		{3, "3"},
		{2, "2"}
	};
	std::map<int, std::string> v = {
		{1, "1"},
		{3, "3"},
		{2, "2"}
	};
	// iterates in the same way
	std::cout << "std::unordered_map" << std::endl;
	for( const auto & n : u)
		std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
	std::cout << std::endl;
	std::cout << "std::map" << std::endl;
	for( const auto & n : v)
		std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
}
std::unordered_map
Key:[2] Value:[2]
Key:[3] Value:[3]
Key:[1] Value:[1]

std::map
Key:[1] Value:[1]
Key:[2] Value:[2]
Key:[3] Value:[3]

4.3 Tuples

了解Python的程序员应该知道元组的概念。看看传统c++中的容器,除了std::pair之外,似乎没有现成的结构来存储不同类型的数据(通常我们将自己定义结构)。但是std::pair的缺陷很明显,只能保存两个元素。

4.3.1 Basic operations

使用元组有三个核心函数:

  1. std::make_tuple: construct tuple
  2. std::get: Get the value of a position in the tuple
  3. std::tie: tuple unpacking
#include <tuple>
#include <iostream>
auto get_student(int id) {
	if (id == 0)
		return std::make_tuple(3.8, 'A', "John");
	if (id == 1)
		return std::make_tuple(2.9, 'C', "Jack");
	if (id == 2)
		return std::make_tuple(1.7, 'D', "Ive");
	// it is not allowed to return 0 directly
	// return type is std::tuple<double, char, std::string>
	return std::make_tuple(0.0, 'D', "null");
}

int main() {
	auto student = get_student(0);
	std::cout << "ID: 0, "
		<< "GPA: " << std::get<0>(student) << ", "
		<< "Grade: " << std::get<1>(student) << ", "
		<< "Name: " << std::get<2>(student) << '\n';
	double gpa;
	char grade;
	std::string name;
	// unpack tuples
	std::tie(gpa, grade, name) = get_student(1);
	std::cout << "ID: 1, "
		<< "GPA: " << gpa << ", "
		<< "Grade: " << grade << ", "
		<< "Name: " << name << '\n';	
}

除了使用常量获取元组对象之外,c++ 14还添加了使用类型来获取元组对象:

std::tuple<std::string, double, double, int> t("123", 4.5, 6.7, 8);
std::cout << std::get<std::string>(t) << std::endl;
std::cout << std::get<double>(t) << std::endl; // illegal, runtime error
std::cout << std::get<3>(t) << std::endl;

4.3.2 Runtime Indexing

如果您仔细想想,您可能会发现上面代码的问题。std::get<>依赖于编译时常量,所以下面的代码是不合法的:

int index = 1;
std::get<index>(t);

那么你会怎么做呢?答案是使用std::variant<>(由c++ 17引入)来为variant<>提供类型模板参数。你可以用一个variant<>来容纳提供的几种类型的变量(在其他语言中,如Python/JavaScript等,作为动态类型)

#include <variant>
template <size_t n, typename... T>
constexpr std::variant<T...> _tuple_index(const std::tuple<T...>& tpl, size_t i) {
	if constexpr (n >= sizeof...(T))
		throw std::out_of_range(".");
	if (i == n)
		return std::variant<T...>{ std::in_place_index<n>, std::get<n>(tpl) };
	return _tuple_index<(n < sizeof...(T)-1 ? n+1 : 0)>(tpl, i);
}
template <typename... T>
constexpr std::variant<T...> tuple_index(const std::tuple<T...>& tpl, size_t i) {
	return _tuple_index<0>(tpl, i);
}
template <typename T0, typename ... Ts>
std::ostream & operator<< (std::ostream & s, std::variant<T0, Ts...> const & v) {
	std::visit([&](auto && x){ s << x;}, v);
	return s;
}

所以我们可以:

int i = 1;
std::cout << tuple_index(t, i) << std::endl;

4.3.3 Merge and Iteration

另一个常见的需求是合并两个元组,这可以通过std::tuple_cat来完成:

auto new_tuple = std::tuple_cat(get_student(1), std::move(t));

您可以立即看到遍历一个元组的速度有多快?但我们只是介绍了如何在运行时为元组建立一个非常数字的索引,这样遍历就变得简单了。首先,我们需要知道元组的长度,它可以:

template <typename T>
auto tuple_len(T &tpl) {
return std::tuple_size<T>::value;
}

这将迭代元组:

for(int i = 0; i != tuple_len(new_tuple); ++i)
	// runtime indexing
	std::cout << tuple_index(i, new_tuple) << std::endl;

4.4 Conclusion

本章简要介绍现代c++中的新容器。它们的用法类似于c++中现有的容器。它相对简单,您可以选择您需要的容器根据实际场景使用,从而获得更好的性能.

虽然std::tuple是有效的,但标准库提供的功能有限,而且无法满足运行时索引和迭代的需求。幸运的是,我们可以自己实现其他方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值