還在用 const std::string &? 試試 std::string_view 吧!
- 2023-06-07
- Liu, An-Chi 劉安齊
¶ 簡介
std::string
作為 C++ 程式入門最基本的函示庫相信大家都用的很熟了,說到如何把字串傳入其他函數,一般來說常見作法就是 const std::string &
亦或是 const char *
,也就是傳址輸入的概念,傳入參考(reference)或是指標(pointer)所以不需要做記憶體的複製。
舉例來說,以下程式法就非常常見:
#include <string>
#include <iostream>
void print(const std::string &input) {
std::cout << input << std::endl;
}
int main() {
std::string message("hello world");
print(message);
}
而從 C++17 標準之後,我們有新的選擇——std::string_view
,View(視圖)在軟體工程上他的概念是「開發人員以不同的視角或方式來查看和存取相同數據陣列(Array)」,意思是 View 提供了一種輕量的抽象介面,讓開發者以不同的方式操作和處理陣列數據,而無需複製或重新排列數據。這種機制可以在不佔用額外記憶體的情況下,對陣列進行切片、重塑、重新排序、重映射等操作,這樣我們可以避免不必要的物件建立或是記憶體操作。所以不管是 Array View 或是 String View(底層還是陣列),概念其實就是用輕量的操作去存取裡面內部的元素,舉例來說我們可以在 Python 的 NumPy 庫、C++ 的 Eigen 庫中看見這個概念。
現在我們稍微改一下前面的程式碼:
#include <string>
#include <iostream>
#include <string_view> // 記得要引入函示庫
void print(std::string_view input) {
std::cout << input << std::endl;
}
int main() {
std::string message("hello world");
print(std::string_view(message));
}
接著來解釋為啥這樣做會比較好。
¶ std::string_view 優點
使用 std::string_view
有以下好處:
- 輕量、低開銷:使用
string_view
可以對原始字串做任意建立、複製、傳遞操作,都不會對原始的記憶體資料作任何拷貝,反之如果用一般的string
做複製的話成本就會很高 - 相容
std::string
的函數:我們平常對string
做的函數操作幾乎都支持,比方說 Iterator、cout
輸出、substr
、find
等等。 - 更加安全:
string_view
永遠不會有所有權,當你使用string_view
的時候可以大膽的刪除他。 - 更高的彈性:使用
string_view
的時候對不同型態的字串物件會有更高的相容性,例如你可以傳入std::wstring
或winrt::hstring
而不會導致錯誤發生,但const std::string &
你可能型別上就直接編譯不過。 - 更快速的字串操作:我們鮮少對
std::string
做substr
因為這樣會產生新的string
物件,成本極高。但用std::string_view
我們可以用substr
且速度依舊很快。根據我的實驗兩者速度可以差到 17 倍之多。另外string_view
可以做前綴後綴的操作,當傳入const std::string &
的時候我們則無法辦到。 - 更現代:擺脫使用
const std::string &
或const char *
,雖然舊方法的效能和新方法一樣,但我們已經提出了不少好處了
¶ std::string_view 範例
¶ substr 子字串
前面提到,std::string::substr
和 std::string_view::substr
兩者效能差很大,實驗 string
呼叫 substr
會慢 string_view
17 倍,理由也簡單,string
的 substr
會再產生一個新的 string
。
那使用 string
的時候不想使用 substr
來做子字串比對怎麼辦呢?你可以自己操作 string
底層的記憶體,只是就不是很方便。
void print1(const std::string &input) {
if(input.substr(0,3) == "123") { // 這個情況下子字串是 std::string
// ...
}
}
void print2(std::string_view input) {
if(input.substr(0,3) == "123") { // 這個情況下子字串是 std::string_view
// ...
}
}
¶ 移除前綴&後綴的操作
前綴(prefix)與後綴(suffix)的操作也很常見,例如我們想要移除一個檔案的前面的路徑,以及後面的副檔名。
方法一:
// g++ test.cpp -std=c++17; ./a.out
#include <iostream>
#include <string_view>
int main()
{
std::string_view path("my_folder/log.txt");
size_t pos1 = path.find('/');
path.remove_prefix(pos1 + 1);
size_t pos2 = path.rfind('.');
path.remove_suffix(path.size() - pos2);
std::cout << pos1 << ", " << pos2 << ": " << path << std::endl; // 9, 13: log
}
方法二:
// g++ test.cpp -std=c++17; ./a.out
#include <iostream>
#include <string_view>
int main()
{
std::string_view path("my_folder/log.txt");
size_t pos1 = path.find('/');
size_t pos2 = path.rfind('.');
std::string_view new_path = path.substr(pos1 + 1, path.size() - pos2 - 1);
std::cout << pos1 << ", " << pos2 << ": " << new_path; // 9, 13: log
}
這邊給了兩個範例,上面使用 remove_prefix
和 remove_suffix
做範例,注意使用這兩個函數會對「原本」的 string_view
物件做修改。下面範例示範了如何用 substr
達到一樣的效果。
從頭到尾我們沒用到任何字串複製,全程就是只有 string_view
,並且這樣的操作又非常的「高階」,在沒有 string_view
的幫助下,我們想要不建立物件或拷貝記憶體來讓 cout
印出東西就不可能這麼優雅。
¶ 結論
總的來說,使用 std::string_view
主要是為了提高效能和節省資源。由於它僅僅是持有指向現有字串的指標和長度,不需要進行新的記憶體配置或字串複製,因此建立速度快且佔用的記憶體較少。同時,std::string_view
提供了一個輕量級的、唯讀 View,方便對大型字串進行高效的存取。這使得它成為處理字串引用或在函數參數傳遞時的理想選擇。
然而,使用 const std::string &
也有其獨特的用途,特別是當需要修改字串或使用 std::string
特有的功能時。在這種情況下,std::string_view
是無法滿足需求的,因為它僅提供了只讀的視圖。另外如果要把字串傳入其他函示庫的 API 的時候,如果 API 指定要傳入 const std::string &
,我們也無法傳入 string_view
。
當我們要做字串操作的時候,除非我們要對 std::string
進行操作,或是需要產生新的所有權,不然我們應該盡可能使用 std::string_view
。