在 C++ 裡頭有相當多「容器」。從原生的陣列,到標準庫 STL 的 vector, array, list, queue, map, set, …。有時候我們只是想以檢視的角度去看一個容器,或是其中一段內容,而不需要底下龐大的資料結構支撐其運作,也不想要擁有這個容器內的元素,這就是 C++20 中標準庫引入 span 概念的原由。

同樣概念在 C++17 時代就針對 string 這個「容器」有相對應的解決方案,就是 string_view字串視圖。

對,他就是個不具資料擁有權的代理 (proxy) 而已。我們今天就來帶各位同學解析一下 span 的用法還有原理。

長怎樣 / 怎麼用

根據 span標準提案 (P0122R7) 裡的敘述,

std::span 是一個不具所有權且用來檢視連續資料的一個觀察者 (view)。

他就藏在標頭檔 <span> 裡面。各位同學不是隔壁棚

的親戚。

#include <span>

而他的定義在標準中大概 / 可能 / 可以長這樣子:

template<
class T,
std::size_t Extent = std::dynamic_extent
> class span;

意思就是他其實是一個樣版類別 (template class)。依照我們初始化他的容器內容物型別 (可能還有長度) 去推導。

  • T 就是你想檢視的容器內容物的型別,比如你今天想檢視的容器是個 vector<int>,那這兒的 T 就是 int
  • Extent 是非型別樣版參數,代表我們想檢視的容器範圍寬度,可以是一個簡單的非負整數或是 std::dynamic_extent (預設),代表動態寬度。大家看到這裡掛了一個預設引數,就知道在標準庫設計中,span 大部份使用情境之下我們是不太需要在意容器檢視範圍的寬度的。
  • 當然這裡的「動態」當然不是真的可以變長變短,而是指在運行期 (run-time) 初始化 span 時才會知道範圍寬度,編譯期 (compile-time) 還不知道的意思。

包裝 C++ 原生陣列

包裝原生陣列的方法很簡單,就直接把一個原生陣列丟進去 span 的 constructor 創一個 span 物件就好 (C++17 以後,就算是樣版類別,宣告物件時也不用手動丟參數具現化樣版)。如同下面代碼中 main() 裡面的 arr_sp

用 span 包裝 C++ 原生陣列的幾種方法

void print_sp(const std::span<int> &sp)
{
std::cout << "Size: " << sp.size() << std::endl;
for (auto & i : sp)
std::cout << i << ' ';
std::cout << std::endl;
} int main()
{
int arr[] = {0,1,2,3,4,5,6,7,8};
std::span arr_sp{arr};
std::span arr_sp2{arr + 2, arr + 6};
std::span arr_sp3{arr + 3, 4}; std::cout <<arr_sp[2] << std::endl; // 2 print_sp(arr_sp);
// Size: 9
// 0 1 2 3 4 5 6 7 8
print_sp(arr_sp2);
// Size: 4
// 2 3 4 5
print_sp(arr_sp3);
// Size: 4
// 3 4 5 6
}

除了直接包裝整個陣列 (arr_sp) 以外,我們也可以用錨點的方式構造一個 span

  • span 的 constructor 給定陣列的起點還有終點的指標。和標準庫的慣例一樣,作用範圍包含起點、不包含終點。代碼中的 arr_sp2
  • span 的 constructor 給定陣列的起點還有長度。代碼中的 arr_sp3

std::span 這個代理人包裝原生陣列之後,不用說,我們依舊可以像古時候一樣以 [] 去存取底下的陣列元素。

比較炫炮的是,如同print_sp()裡的用法:我們可以現代化的 ranged-base for loop 去列舉代理範圍內的元素 ,呼叫 size()獲得 span 代理的範圍長度,也提供 begin() / end() 這對活寶,也就是通過 C++ 中的 iterator 來存取範圍內元素。

特別要注意的是,[] 如果存取越界的話,這個在標準中說是未定義行為,各家編譯器帶的標準庫或是 open source 實作可以各自表述,所以大家還是要小心點不要亂搞 XD。

古時候把陣列傳來傳去總是需要在函式參數多標示個長度,甚至需要描述區段時,還需要多兩個參數標示起點終點,通過 span 樣版把長度吃進去類別裡面,完全省去這層困擾,且幾近零時間成本,程式也變得簡潔好懂多了。

// Before
int read_info_buf(char buf[], int size) {} char buf[BUF_SIZE];
read_info_buf(buf, BUF_SIZE); // After
int read_info_buf(std::span<char> s) {}
char buf[BUF_SIZE];
read_info_buf(buf);

包裝 C++ 標準庫 STL 的容器 (Container)

身為 C++20 的新發明,span 肯定可以拿來包 C++ 標準庫裡面那堆容器 (Container) 了是吧?是,也不是。

記得我們前面提到 span 代表的是一連串連續的資料。因此能被 span 包裝的容器自然也就必須是連續的容器 (sequential contiguous container):也就是只有 arrayvector(除開 vector<bool>),stringspan

補充一下,舉例來說,在 clang 9.0.1/LLVM 的 span constructor SFINAE 實作方法是判斷

  • std::data(Container) 是否是 well-formed。
  • 而且 std::data(Container) 這個函式回傳的指標所指向的型別形成的陣列能否轉型成 span 想包裝的內容物的陣列。

上面看不懂沒關係,重點是怎麼用,其實跟上面包裝原生的 C++ 陣列 87% 像:直接丟容器給 span。不多說直接上圖

我們從上面代碼的 main() 裡面總結一下 span 幾種包裝 STL 的方法:

  • 直接以一個 array 初始化 span。
  • arraybegin()arrayend() 初始化。
  • a_sp 呼叫 subspan(2, 2) 得到一個從 2 開始、長度為 2 的 span,並拿來初始化一個新的 span
  • 以一個 vector (除開 vector<bool>) 初始化 span
  • 以一個 string 初始化 span

另外也稍微提一下 span 提供的幾個分段函式:

  • first(3): 對一個 span 取前 3 個元素成為一個新的 span
  • last(5): 對一個 span 取後 5 個元素成為一個新的 span
  • subspan(2, 2): 對一個 span 從位置 2 開始,取兩個元素成為一個新的 span

靜態長度 Static extent

前面稍為提到惹,所謂靜態的長度是指在編譯時期就直接知道想包裝的範圍寬度多長。像是前面舉例的代碼裡頭,span 拿來包裝 C++ 原生陣列,或是包裝 **std::array** 在 constructor 都會推導出 static extent,在這裡我自己把他取名叫靜態 **span**

對我們包裝一個容器來說,span 擁有 static extent 帶給我們最大的好處就是可以把代碼寫得更加炫炮。

結合 C++17 Structured Binding

在提案 p1024r3 裡面提到了:既然原生陣列和標準庫的 array 從 C++17 起就支援了 structured binding,那麼能包裝這些東西的 span 也應該要能支援 structured binding,才能達到完整的 array reference 的效用。所以這個提案改動了

  • 實作 std::get 對靜態 span 的支援。
  • 實作 std::tuple_sizestd::tuple_elementspan 的支援。
  • 也就是可以通過 structured binding 直接解構一個靜態 **span**

按照 C++17 的 structured binding 語法完美套用,無縫接軌。直接上圖

編譯器支援 / Open Source

上面所有代碼都在 MacOS 10.15.2 使用 clang/LLVM 9.0.1 編譯測試過,這篇文章寫作當下幾家編譯器大廠也就 clang 的 libc++ 有釋出 std::span 的實作。GCC 說 libstdc++ 10 版會出,而 MSVC 就…再看看 XD

其實 span 的呼聲一直都有,最早可以追溯到 2012 年的 array reference,只是進標準的進程一直很緩慢就是了,所以網路上也產生了各家實作的開源 span 版本,大家可以不用裝 clang 就簡單上手一下,

值得一提的是,隨著 span 的標準進展,除了各種開源實作,另一種 span 的擴展的呼聲和其開源也逐漸冒了出頭,那就是把包裝的連續資料視為多維空間的 mdspan。有興趣的同學也可以去看看 XD 大概是醬 888

C++20 | std::span 陣列、容器的代理人的更多相关文章

  1. std::vector<Channel2*> m_allChannels;容器,以及如何根据channelid的意义

    std::vector<Channel2*> m_allChannels;容器,以及如何根据channelid的意义 这个容器保存了所有客户端连接的channel Channel2* Li ...

  2. [JS奇怪的世界]No.55 危險小叮嚀:陣列與for in

    前言 前面已經瞭解了使用內建函數建構子的某些危險地方,但其實陣列與for in,也是有一些危險的地方. 陣列與for in 在前面幾個章節有講過陣列就是物件,所以我們一樣可以使用 for in來做處理 ...

  3. 使用std::find_if提取序列容器的子串

    一个需求是这样的,一个vector容器中,我需要提取满足一定条件的元素的序列.就比如,一个树形结构,我把该接口拍扁成vector容器,每个节点都有一个惟一ID. 以下就是根据特定的ID查找节点下的子节 ...

  4. PAT Basic 1043 输出PATest (20分)[Hash散列]

    题目 给定⼀个⻓度不超过10000的.仅由英⽂字⺟构成的字符串.请将字符重新调整顺序,按"PATestPATest-."这样的顺序输出,并忽略其它字符.当然,六种字符的个数不⼀定是 ...

  5. Ruby学习笔记0708

    #!/usr/bin/env ruby class MegaGreeter attr_accessor :names # 初始化這個物件 def initialize(names = "Wo ...

  6. 【表格设置】HTML中合并单元格,对列组合应用样式,适应各浏览器的内容换行

    1.常用表格标签 普通    <table>           |           <tr>          |           |          <th ...

  7. 2.9 C++STL map/multimap容器详解

    文章目录 2.9.1 引入 2.9.2 代码示例 map案列 multimap案列 2.9.3 代码运行结果 总结 2.9.1 引入 map相对于set区别,map具有键值和实值,所有元素根据键值自动 ...

  8. unordered容器

    1.散列容器(hash container)  散列容器通常比二叉树的存储方式可以提供更高的访问效率. #include <boost/unordered_set.hpp> #includ ...

  9. c++ 标准库的各种容器(vector,deque,map,set,unordered_map,unordered_set,list)的性能考虑

    转自:http://blog.csdn.net/truexf/article/details/17303263 一.vector vector采用一段连续的内存来存储其元素,向vector添加元素的时 ...

  10. EasyUI 格式化DataGrid列

    easyui DataGrid中格式化列,如果单价低于20,则使用定义列formatter为红色文本.格式化DataGrid列,我们应该设置formatter属性,这个属性是一个函数.格式化函数包括两 ...

随机推荐

  1. python列表之索引及len()函数

    我们在刚开始使用列表的时候,经常会遇到这种错误 list_1 = ['one', 'two', 'three', 'four', 'five'] print(list_1[5]) 这段代码看上去是没有 ...

  2. MODBUS转PROFINET网关TS-180 网关连接西门子 PLC 和工业称重仪表

    随着科技的高速发展,工业自动化行业对日益多样的称重需求越来越高,上海某公司在国内的一个 工业自动化项目中,监控中心系统需要远程实时采集工业称重仪表测量的各种称重参数.该系统使用的是 西门子 S7-30 ...

  3. 聊聊分布式 SQL 数据库Doris(六)

    负载均衡 此处的负载均衡指的是FE层的负载均衡. 当部署多个 FE 节点时,用户可以在多个 FE 之上部署负载均衡层来实现 Doris 的高可用.官方文档描述: 负载均衡 . 实现方式 实现方式有多种 ...

  4. day01预习-基本语法

    typora-copy-images-to: media 基本语法 JavaScript的历史: ​ 在95年以前,就有很多上网的用户了,当时的带宽只有28.8kb/s,用户要进行表单的验证时,点击提 ...

  5. 【Java】Java中StringBuilder()成员方法append()和toString()

    StringBuilder就相当于C++的String长度可变,用于构造字符串对象,内部使用自动扩容的数组操作字符串数据. StringBuilder和StringBuffer使用的是相同的API[区 ...

  6. 吉特日化MES配料工艺参数标准版-第二版

    作者:情缘 出处:http://www.cnblogs.com/qingyuan/ 关于作者:从事仓库,生产软件方面的开发,在项目管理以及企业经营方面寻求发展之路 版权声明:本文版权归作者和博客园共有 ...

  7. 通过 VS Code 优雅地编辑 Pod 内的代码(非 NodePort)

    目录 1. 概述 2. NodePort 方式 3. Ingress 方式 4. 救命稻草 5. 其他 1. 概述 今天聊点啥呢,话说,你有没有想过怎样用 VS Code 连上 K8s 集群内的某个 ...

  8. 云端开炉,线上训练,Bert-vits2-v2.2云端线上训练和推理实践(基于GoogleColab)

    假如我们一定要说深度学习入门会有一定的门槛,那么设备成本是一个无法避开的话题.深度学习模型通常需要大量的计算资源来进行训练和推理.较大规模的深度学习模型和复杂的数据集需要更高的计算能力才能进行有效的训 ...

  9. 数字孪生和GIS融合后能够为城市交通带来哪些便利?

    数字孪生和GIS的融合对于城市交通领域带来了诸多便利,从智能交通管理到出行体验的提升,为城市交通带来了全新的发展机遇. 首先,数字孪生技术与GIS的结合可以实现智能交通管理.通过GIS建立城市交通网络 ...

  10. 前端异步编程——async/await

    async 从字面上看就是"异步",它放在函数定义之前,是使该函数在调用时开一个子线程,以不影响主线程的运行. 而 await 经常和 async 组合使用,在 async 定义的 ...