Tuesday, July 4, 2023

淺談 C++ 20 的 std::span 跨類

引言
自 C++ 20 開始,針對所有順序排列的矩陣,標準庫開始提供了一種新的寫法。

以往,我們在處理不同容器 (Container) 的時候,即使它們十分相似,我們也要分開續一處理。比如說,std::array,std::vector,還有最傳統的 C-style array,即使它們在記憶體中的排列,基本上是完全一樣的,但只因名稱不同,我們就需要寫三個不同的函式去處理它。而針對傳統的 C-style array,我們更需要自行使用 sizeof(array)/sizeof(type) 去找出陣列的大小,情況的確十分麻煩。

而自從 C++ 20 之後,情況簡單太多了:一個 std::span 即可解決一切。

---

範例及解說
你可以把 std::span 想像成是所有順序排列矩陣的模板,然後再加上 string_view 的邏輯。

這裏的模板是指,我們只需要使用 std::span 實作一次,不同的矩陣都能自動支援該函式。而 string_view 是指,std::span 只一個視圖:它的作用,只是指向某一個矩陣,它本身並不擁有該矩陣的記憶體。雖然我們能透過 std::span 去讀寫矩陣中每一個元素 (element), 但我們卻不能改變該矩陣本身的生命周期 (lifecycle)。換句話說,它只是指向該物件,但不能保證該物件仍然在 stack / heap 中。

以下這一個簡單的範例,就可解釋模板 (Template Class) 的情況:

#include <iostream>
#include <vector>
#include <span>

template<typename T>
void printSequence(std::span<T> sequence)
{
    for (T& s: sequence)
    {
        std::cout << s << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> v1   = {1, 2, 3, 4};
    std::array<int, 4> a1 = {2, 3, 4, 5};
    int ca1 []            = {3, 4, 5, 6};

    std::cout << "printSequence vector" << std::endl;
    printSequence<int>(v1);

    std::cout << "printSequence array" << std::endl;
    printSequence<int>(a1);

    std::cout << "printSequence c-style array" << std::endl;
    printSequence<int>(ca1);

    return 0;
}

程式的執行結果,則是如下:

printSequence vector
1 2 3 4
printSequence array
2 3 4 5
printSequence c-style array
3 4 5 6 

由此可見,std::span 就像一個支援 std::vector / std::array / c-style array 的模板一樣,我們只需要使用 std::span 實作函式一次,即可自動獲得所有不同矩陣類型的函式支援。

以下的另一個例子,則說明 std::span 與 string_view 相似的地方。
std::span 只是一個視圖 (View),所以它實際上只是指針,並不能控制矩陣的生命周期:

#include <iostream>
#include <vector>
#include <span>

template<typename T>
void printSequence(std::span<T> sequence)
{
    for (T& s: sequence)
    {
        std::cout << s << " ";
    }
    std::cout << std::endl;
}

int main() {

    std::span<int> my_span;

    {
        std::vector<int> my_vector = {1, 2, 3, 4};
        my_span = std::span<int>{my_vector};

        std::cout << "inside scope" << std::endl;
        printSequence<int>(my_span);
    }

    std::cout << "outside scope" << std::endl;
    printSequence<int>(my_span);

    return 0;
}

程式執行結果如下:

inside scope
1 2 3 4
outside scope
-353796048 25208 2043 0 

由於 my_vector 在 "outside scope" 那行已經被消滅掉,所以打印出來的 my_span 只會是一堆亂碼。由此可見,my_span 並不能改變 my_vector 的作用域 (scope),它充其量只是指針。

---

小結
使用 std::span 可以簡單地解決不同矩陣的列舉和更新。尤其是傳統的 c-style array,使用 std::span 即可使 c-style array 也能自動更援不同 iterator 和 for-each 等新功能。不過,由於 std::span 不能控制物件的生命周期,所以使用的時候也要小心,避免存取一些早已消失了的物件。

---

參考:https://learn.microsoft.com/en-us/cpp/standard-library/span-class?view=msvc-170