本篇文章從string_view
引入的背景出發,依次介紹了其相關的知識點及使用方式,而後對常見的使用陷阱進行了說明,最後對該類型作總結。ios
在平常C/C++編程中,咱們常進行數據的傳遞操做,好比,將數據傳給函數。當數據佔用的內存較大時,減小數據的拷貝能夠有效提升程序的性能。在C中指針是完成這一目的的標準數據結構,而C++引入了安全性更高的引用類型。因此在C++中若傳遞的數據僅僅只讀,const string&
成了C++的自然的方式。但這並不是完美,從實踐來看,它至少有如下幾方面問題:數據庫
string
類型不一樣,傳入時,編譯器須要作隱式轉換,即須要拷貝這些數據生成string
臨時對象。const string&
指向的其實是這個臨時對象。一般字符串字面值較小,性能損耗能夠忽略不計;但字符串指針和字符數組某些狀況下可能會比較大(好比讀取文件的內容),此時會引發頻繁的內存分配和數據拷貝,會嚴重影響程序的性能。substr
O(n)複雜度std::string
提供了這個函數,美中不足的是其每次都返回一個新生成的子串,很容易引發性能熱點。實際上咱們本意並非要改變原字符串,爲何不在原字符串基礎上返回呢?在C++17中引入了string_view
,能很好的解決以上兩個問題。編程
從名字出發,咱們能夠類比數據庫視圖,view
表示該類型不會爲數據分配存儲空間,並且該數據類型只能用來讀。該數據類型可經過{數據的起始指針,數據的長度}
兩個元素表示,實際上該數據類型的實例不會具體存儲原數據,僅僅存儲指向的數據的起始指針和長度,因此這個開銷是很是小的。數組
要使用字符串視圖,須要引入<string_view>
,下面介紹該數據類型主要的API。這些API基本上都有constexpr
修飾,因此能在編譯時很好地處理字符串字面值,從而提升程序效率。安全
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"))
實際執行了兩步操做:性能
string("abc")
轉換爲string_view
對象a string_view
使用對象本篇文章從string_view
引入的背景,自定義字面量也是C++17新增的特性,提升了常量的易讀。
下面的代碼取值cppreference,能很好地說明自定義字面值和字符串語義的差別。spa
#include <string_view> #include <iostream> int main() { using namespace std::literals; std::string_view s1 = "abc\0\0def"; std::string_view s2 = "abc\0\0def"sv; std::cout << "s1: " << s1.size() << " \"" << s1 << "\"\n"; std::cout << "s2: " << s2.size() << " \"" << s2 << "\"\n"; }
輸出:翻譯
s1: 3 "abc" s2: 8 "abc^@^@def"
以上例子能很好看清兩者的語義區別,\0
對於字符串而言,有其特殊的意義,即表示字符串的結束,字符串視圖根本不care,它關心實際的字符個數。
下面列舉其成員函數:忽略了函數的返回值,若函數有重載,括號內用...
填充。這樣能夠對其有個總體輪廓。
// 迭代器 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
基本一致。有幾個地方須要特別說明:
string_view
的substr
函數的時間複雜度是O(1),解決了背景部分的第二個問題。string_view
的數據指向,不會修改指向的數據。除此以外,函數名基本是自解釋的。
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-版本
#include <string> #include <iostream> #include <vector> #include <algorithm> #include <sstream> #include <string_view> void lines(std::vector<std::string> &lines, std::string_view str) { auto sep{"\n"}; size_t start{str.find_first_not_of(sep)}; size_t end{}; while (start != std::string_view::npos) { end = str.find_first_of(sep, start + 1); if (end == std::string_view::npos) end = str.length(); lines.push_back(std::string{str.substr(start, end - start)}); start = str.find_first_not_of(sep, end + 1); } }
上面的例子僅僅是把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
也會帶來一系列的問題。
string_view
範圍內的字符可能不包含\0
如
#include <iostream> #include <string_view> int main() { std::string_view str{"abc", 1}; std::cout << str.data() << std::endl; return 0; }
原本是要打印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++沒有這樣的檢查,須要人工保證。下面列出一些典型的問題狀況:
std::string_view sv = std::string{"hello world"};
string_view foo() { std::string s{"hello world"}; return string_view{s}; }
auto id(std::string_view sv) { return sv; } int main() { std::string s = "hello"; auto sv = id(s + " world"); }
string_view
解決了一些痛點,但同時也引入了指針和引用的一些老問題。C++標準並無對這個類型作太多的約束,這引來的問題是咱們能夠像日常的變量同樣以多種方式使用它,如,能夠傳參,能夠做爲函數返回值,能夠作廣泛變量,甚至咱們能夠放到容器裏。隨着使用場景的複雜,人工是很難保證指向的內容的生命週期足夠長。因此,推薦的使用方式:僅僅做爲函數參數,由於若是該參數僅僅在函數體內使用而不傳遞出去,這樣使用是安全的。
請關注個人公衆號哦。