string_view理解与用法(二)
以前寫了《string_View理解與用法(一)》和《詳解C++17下的string_view》,請參考。
本篇文章從string_view引入的背景出發,依次介紹了其相關的知識點及使用方式,然后對常見的使用陷阱進行了說明,最后對該類型做總結。
一、背景
在日常C/C++編程中,我們常進行數據的傳遞操作,比如,將數據傳給函數。當數據占用的內存較大時,減少數據的拷貝可以有效提高程序的性能。在C中指針是完成這一目的的標準數據結構,而C++引入了安全性更高的引用類型。所以在C++中若傳遞的數據僅僅只讀,const string&成了C++的天然的方式。但這并非完美,從實踐來看,它至少有以下幾方面問題:
這三類低級數據類型與string類型不同,傳入時,編譯器需要做隱式轉換,即需要拷貝這些數據生成string臨時對象。const string&指向的實際上是這個臨時對象。通常字符串字面值較小,性能損耗可以忽略不計;但字符串指針和字符數組某些情況下可能會比較大(比如讀取文件的內容),此時會引起頻繁的內存分配和數據拷貝,會嚴重影響程序的性能。
這是一個特別常用的函數,好在std::string提供了這個函數,美中不足的是其每次都返回一個新生成的子串,很容易引起性能熱點。實際上我們本意并不是要改變原字符串,為什么不在原字符串基礎上返回呢?
在C++17中引入了string_view,能很好的解決以上兩個問題。
二、std::string_view
從名字出發,我們可以類比數據庫視圖,view表示該類型不會為數據分配存儲空間,而且該數據類型只能用來讀。該數據類型可通過{數據的起始指針,數據的長度}兩個元素表示,實際上該數據類型的實例不會具體存儲原數據,僅僅存儲指向的數據的起始指針和長度,所以這個開銷是非常小的。
要使用字符串視圖,需要引入<string_view>,下面介紹該數據類型主要的API。這些API基本上都有constexpr修飾,所以能在編譯時很好地處理字符串字面值,從而提高程序效率。
2.1 構造函數
constexpr string_view() noexcept; constexpr string_view(const string_view& other) noexcept = default; constexpr string_view(const CharT* s, size_type count); constexpr string_view(const CharT* s);基本上都是自解釋的,唯一需要說明的是:為什么我們代碼string_view foo(string("abc"))可以編譯通過,但為什么沒有對應的構造函數?
實際上這是因為string類重載了string到string_view的轉換操作符:
operator std::basic_string_view<CharT, Traits>() const noexcept;
所以,string_view foo(string("abc"))實際執行了兩步操作:
2.2 自定義字面量
自定義字面量也是C++17新增的特性,提高了常量的易讀。
下面的代碼取值cppreference,能很好地說明自定義字面值和字符串語義的差異。
輸出:
s1: 3 "abc" s2: 8 "abc^@^@def"以上例子能很好看清二者的語義區別,\0對于字符串而言,有其特殊的意義,即表示字符串的結束,字符串視圖根本不care,它關心實際的字符個數。
2.3 成員函數
下面列舉其成員函數:忽略了函數的返回值,若函數有重載,括號內用...填充。這樣可以對其有個整體輪廓。
// 迭代器 begin() end() cbegin() cend() rbegin() rend() crbegin() crend()// 容量 size() length() max_size() empty()// 元素訪問 operator[](size_type pos) at(size_type pos) front() back() data()// 修改器 remove_prefix(size_type n) remove_suffix(size_type n) swap(basic_string_view& s)copy(charT* s, size_type n, size_type pos = 0) string_view substr(size_type pos = 0, size_type n = npos) compare(...) starts_with(...) ends_with(...) find(...) rfind(...) find_first_of(...) find_last_of(...) find_first_not_of(...) find_last_not_of(...)從函數列表來看,幾乎跟string的只讀函數一致,使用string_view的方式跟string基本一致。有幾個地方需要特別說明:
除此之外,函數名基本是自解釋的。
2.4 示例
Haskell中有一個常用函數lines,會將字符串切割成行存儲在容器里。下面我們用C++來實現
string-版本
#include <string> #include <iostream> #include <vector> #include <algorithm> #include <sstream>void lines(std::vector<std::string> &lines, const std::string &str) {auto sep{"\n"};size_t start{str.find_first_not_of(sep)};size_t end{};while (start != std::string::npos) {end = str.find_first_of(sep, start + 1);if (end == std::string::npos)end = str.length();lines.push_back(str.substr(start, end - start));start = str.find_first_not_of(sep, end + 1);} }?
上面我們用const std::string &類型接收待分割的字符串,若我們傳入指向較大內存的字符指針時,會影響程序效率。
使用std::string_view可以避免這種情況:
string_view-版本
上面的例子僅僅是把string類型修改成了string_view就獲得了性能上的提升。一般情況下,將程序中的string換成string_view的過程是比較直觀的,這得益于兩者的成員函數的相似性。但并不是所有的“翻譯”過程都是這樣的,比如:
void lines(std::vector<std::string> &lines, const std::string& str) {std::stringstream ss(str);std::string line;while (std::getline(ss, line, '\n')) {lines.push_back(line);} }這個版本使用stringstream實現lines函數。由于stringstream沒有相應的構造函數接收string_view類型參數,所以沒法采用直接替換的方式,所以翻譯過程要復雜點。
三、使用陷阱
世上沒有免費的午餐。不恰當的使用string_view也會帶來一系列的問題。
如
?
本來是要打印a,但輸出了abc。這是因為字符串相關的函數都有一條兼容C的約定:\0代表字符串的結尾。上面的程序打印從開始到字符串結束的所有字符,雖然str包含的有效字符是a,但cout認\0。好在這塊內存空間有合法的字符串結尾符,如果str指向的是一個沒有\0的字符數組,程序很有可能會出現內存問題,所以我們在將string_view類型的數據傳入接收字符串的函數時要非常小心。
2.從[const] char*構造string_view對象時間復雜度O(n)
這是因為獲取字符串的長度需要從頭開始遍歷。如果對[const] char*類型僅僅是一些O(1)的操作,相比直接使用[const] char*,轉為string_view是沒有性能優勢的。只不過是相比const string&,string_view少了拷貝的損耗。實際上我們完全可以用[const] char*接收所有的字符串,但這個類型太底層了,不便使用。在某些情況下,我們轉為string_view可能僅僅是想用其中的一些函數,比如substr。
3.string_view指向的內容的生命周期可能比其本身短
string_view并不擁有其指向內容的所有權,用Rust的術語來說,它僅僅是暫時borrow(借用)了它。如果擁有者提前釋放了,你還在使用這些內容,那會出現內存問題,這跟懸掛指針(dangling pointer)或懸掛引用(dangling references)很像。Rust專門有套機制在編譯時分析變量的生命期,保證borrow的資源在使用期間不會被釋放,但C++沒有這樣的檢查,需要人工保證。下面列出一些典型的問題情況:
四、總結
string_view解決了一些痛點,但同時也引入了指針和引用的一些老問題。C++標準并沒有對這個類型做太多的約束,這引來的問題是我們可以像平常的變量一樣以多種方式使用它,如,可以傳參,可以作為函數返回值,可以做普遍變量,甚至我們可以放到容器里。隨著使用場景的復雜,人工是很難保證指向的內容的生命周期足夠長。所以,推薦的使用方式:僅僅作為函數參數,因為如果該參數僅僅在函數體內使用而不傳遞出去,這樣使用是安全的。
本文轉自:https://segmentfault.com/a/1190000018387368
總結
以上是生活随笔為你收集整理的string_view理解与用法(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于项目延迟交付
- 下一篇: HiveSQL 数据定义语言(DDL)