4.1 Linear Container
4.1.1 std::array
当你看到这个容器时,你肯定会遇到这样的问题:
- 为什么直接介绍std::array而不是std::vector ?
- 已经有传统数组了,为什么要用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
使用元组有三个核心函数:
- std::make_tuple: construct tuple
- std::get: Get the value of a position in the tuple
- 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是有效的,但标准库提供的功能有限,而且无法满足运行时索引和迭代的需求。幸运的是,我们可以自己实现其他方法。