現代C++教程:高速上手(四)-容器

Ryan669發表於2020-09-19

1、線性容器

std::array與std::vector不同的是,array物件的大小是固定的,如果容器大小是固定的,那麼可以優先考慮使用std::array容器。

由於std::vector是自動擴容的,當存入大量的資料後,並且對容器進行了刪除操作,容器並不會自動歸還被刪除元素相應的記憶體,這時候需要手動執行shrink_to_fit()釋放這部分記憶體。

std::array C風格介面傳參:

void foo(int *p, int len){
    return;
}
std::array<int, 4> arr = {1,2,3,4};
//foo(arr,arr.size()); //非法,無法隱式轉換
foo(&arr[0], arr.size());
foo(arr.data(), arr.size());

//使用std::sort
std::sort(arr.begin(), arr.end());
//升序
std::sort(arr.begin(), arr.end(), [](int a, int b){
    return b > a;
})

std::forward_list是一個列表容器,使用方法和std::list基本類似。和list的雙向連結串列的實現不同,forward_list使用單向連結串列進行實現,提供了O(1)複雜度的元素插入,不支援快速隨機訪問,也是標準庫容器中唯一一個不提供size()方法的容器。當不需要雙向迭代時,具有比list更高的空間利用率。

2、無序容器

傳統c++中的有序容器 std::map / std::set,這些元素內部通過紅黑樹進行實現,插入和搜尋的平均複雜度均為O(log(size))。在插入元素時,會根據<操作符比較元素大小並判斷元素是否相同,並選擇合適的位置插入到容器中。當對這個容器中的元素進行遍歷時,輸出結果會按照<操作符的順序來逐個遍歷。

而無序容器中的元素是不進行排序的,內部通過Hash表實現,插入和搜尋元素的平均複雜度為O(constant),在不關心容器內部元素順序時,能夠獲得顯著的效能提升。

c++11引入了兩組無序容器:std::unordered_map / std::unordered_multimap和std::unordered_set / std::unordered_multiset。
它們的用法和原有的std::map / std::multimap / std::set / std::multiset基本類似。

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

using namespace std;

int main(){
    unordered_map<int, string> u = {
        {1, "1"},
        {3, "3"},
        {2, "2"}
    };

    map<int, string> v = {
        {1, "1"},
        {3, "3"},
        {2, "2"}  
    };

    cout << "std::unordered_map" << endl;
    for(const auto &n : u){
        cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
    }

    cout <<endl;
    cout << "std::map" << endl;
    for (const auto & n : v){
        cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
    }
}

3、元組

傳統c++中的容器,除了std::pair外,似乎沒有現成的結構能夠用來存放不同型別的資料。但std::pair的缺陷是顯而易見的,只能儲存兩個元素。

元組基本操作

三個核心函式:
1、std::make_tuple: 構造元組
2、std::get:獲得元組某個位置的值
3、std::tie:元組拆包

#include <iostream>
#include <tuple>

using namespace std;

auto get_student(int id){
    switch (id)
    {
    case 0:
        return make_tuple(3.8, 'A', "張三");
        break;
    case 1:
        return make_tuple(2.9, 'C', "李四");
        break;
    case 2:
        return make_tuple(1.7, 'D', "王五");
        break;
    default:
        return make_tuple(0.0, 'D', "null");
        break;
    }
}

int main(){
    auto student = get_student(0);
    std::cout << "ID: 0, "
    << "GPA: " << get<0>(student) << ", "
    << "成績:" << get<1>(student) << ", "
    << "姓名:" << get<2>(student) << "\n";

    double gpa;
    char grade;
    string name;

    //元祖進行拆包
    tie(gpa, grade, name) = get_student(1);
    std::cout << "ID: 1, "
    << "GPA: " << gpa << ", "
    << "成績:" << grade << ", "
    << "姓名:" << name << "\n";

    return 0;
}

std::get除了使用常量獲取元組物件外,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; //非法,引發編譯期錯誤
std::cout << std::get<int>(t) << std::endl;

執行期索引

std::get<>依賴一個編譯期的常量,所以下面的方式是不合法的:

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

c++17引入了std::variant<>,提供給variant<>的型別模版引數 可以讓一個variant<>從而容納提供的幾種型別的變數(在其他語言,例如Python/JavaScrpit等,表現為動態型別):

#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(student, i) << endl;

元組合並與遍歷

還有一個常見的需求就是合併兩個元組,這可以通過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){
     //執行期索引
    std::cout << tuple_index<i, new_tuple) << std::endl;
}

總結

std::tuple雖然有效,但是標準庫提供的功能有限,沒辦法滿足執行期索引和迭代的需求,好在我們還有其他辦法可以自行實現。

相關文章