C++20 的 span

std::spanC++20 加入的一個新的類別(文件),他的基本概念是「對連續資料做處理的一個觀察者(view)」;由於只是「觀察者」,所以他並不具有所有權,某種意義上,可以看成類似一組連續資料的局部參考。

在最簡單的用法上,個人覺得它可以用來處理傳統陣列的傳遞;某種意義上算是做個封包、包裝成 STL 的容器的形式。下面是一個簡單的範例:

#include <iostream>
#include <span>
#include <algorithm>
 
int main()
{
  int aData[]{ 5, 4, 3, 2, 1 };
 
  std::span sData(aData);
  std::cout << sData.size() << "\n"; // 5
  std::ranges::sort(sData);
}

在上面的例子可以看到,這邊是透過 sData 來重新封包傳統陣列 aData,之後就可以按照使用 C++ 容器的習慣來操作了~同時,這也讓他更方便地利用 C++ 提供的演算法(這邊是拿 sort() 來舉例)。

而實際上,他也可以只用來對應局部的資料(但是還是要連續),例如:

std::span sData(aData + 1, aData + 4);
std::cout << sData.size() << "\n"; // 3
std::ranges::sort(sData);

for (int i = 0; i < 5; ++i)
  std::cout << aData[i] << ",";

這邊 sData 就僅會對應到 aData 的第 2 – 4 的資料;也因此在對 sData 進行排序後,再把整個 aData 輸出的話,結果會變成是:

5,2,3,4,1,

另外,std::span 也有提供 subspan() 這個函式,可以更直覺地產生僅對應部分區域的 span 物件;像在下面的程式中,sData1sData2 就是對應到相同的資料。

std::span sData1(aData + 1, aData + 4);
auto sData2 = std::span(aData).subspan(1, 3);

以個人來說,個人會覺得 subspan() 的寫法比較直覺;而除了 subspan() 外,他也還有 first()last() 可以使用。

而除了針對傳統的陣列進行操作外,std::span 也理所當然地可以用 STL 的連續型容器上;以 std::vector 來說,就是下面的形式:

std::vector<int> vData{ 5, 4, 3, 2, 1 };
auto sData = std::span(vData).subspan(1, 3);

如果有必要僅針對容器的部分範圍操作的話,在使用上也算相當方便。


而在 Heresy 來看,std::span 最容易實用的地方,應該是拿來當作函式的引數來使用。

在傳統的 C++ 裡面,如果要傳遞一個陣列的話,還需要同時傳遞他的大小,才能進行操作;下面就是一個簡單的例子:

void output(const int aData[], size_t uSize)
{
  std::cout << "Size: " << uSize << "\n";
  for (size_t i = 0; i < uSize; ++ i)
    std::cout << aData[i] << "\n";
}
 
int main()
{
  int aData[]{ 5, 4, 3, 2, 1 };
  output(aData, 5);
}

如果一個沒弄好,陣列大小忘了修改的話,就可能會出現大問題。

而如果透過 std::span 的話,則可以改寫成:

void output(const std::span<int>& aData)
{
  std::cout << "Size: " << aData.size() << "\n";
  for (int& d : aData)
    std::cout << d << "\n";
}

如此一來,output() 這個函式就可以同時適用於傳統的陣列以及符合轉換條件的容器了!

int aData[]{ 5, 4, 3, 2, 1 };
output(aData);

std::vector<int> vData{ 5, 4, 3, 2, 1 };
output(vData);

output(std::span(vData).last(3));

而如果僅要針對部分資料處理,也可以透過 subspan() 等函式簡單地做到,在個人來看,對特定狀況下的程式開發,算是可以做到相當地簡化的!

而且,透過 std::span 除了可以很方便地用同樣的介面操作局部的資料外,也不會有額外複製資料的成本,算是滿實用的。


如果編譯器還不支援 C++20 的話,目前在 GSLBoostboost::span文件)裡、也都有提供同樣的東西可以使用。

而除了 std::span 外,在 C++17 的時候也有提供 std::string_view文件);他和 std::span 有點像,應該可以視為針對字串特化的唯讀版本(參考《Differences between std::string_view and std::span》)。

如果輸入的字串不需要修改的話,那把函式的引數改成用 std::string_view,也可以增加使用上的彈性。

#include <iostream>
#include <string_view>
 
void output(const std::string_view sText)
{
  std::cout << sText << "\n";
}
 
int main()
{
  output("Hello world");
 
  const char* sText1 = "Hello world";
  output(sText1);
 
  std::string sText2 = "Hello world";
  output(sText2);
}

同時,由於 std::string_view 不需要做資料的複製,所以如果是要取部分字串出來做比較的話,也會比使用 substr() 來的有效率許多~

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *