為什麼大多數的C++的開源庫都喜歡自己實現一個string?
如果是需要用在高性能處理的場景下,自己實現一個string無可厚非,比如網路庫;然而現在一些工具庫,或者UI庫,也都實現了一套string,這是為什麼呢?難道stl的string就這麼不堪?
添加:另補:Quora上也有類似問題,https://www.quora.com/Why-do-many-projects-implement-their-own-string-vector-map-etc-instead-of-using-STL?share=970af15bsrid=uEavz(也可以注意一下語氣的不同)
C++一個詭異的設計,就是他的string和流,居然不是基於編碼,而是基於所謂的char_traits的,而char_traits裡面指定的那些東西,都是一些關於字元的複製,移動,構造等操作,和編碼一點關係都沒有。
你能告訴我string,wstring是什麼編碼嗎?不能,甚至wchar_t有多寬都是不統一的。
而且string的設計根本沒有考慮到變長編碼。
不管這些,string甚至沒有提供一些方便的字元串操作,split要用stringstream,搜索符串要用C++17才提供的algorithm裡面的boyer_moore_searcher,to_upper要用algorithm裡面的transform,什麼都得自己寫。
char16_t char32_t是規定了編碼,但是除此之外什麼都沒有。
u8前綴的string literal C++14(還是17)才加入
有人說locale,locale這個implementation defined的東西,實際用起來,又是一坨翔,再加上沒有幾個標準庫能正確實現的codecvt,加在一起成為了C++最混亂黑暗的角落。
C++不是號稱不限制你的開發方式么,每個庫想怎麼搞就怎麼搞,這明明就是 C++的優勢,不知道你們抱怨個啥?哈哈
接著說 std::string 的性能問題,舉個具體例子吧,之前接手過一個項目,別的部門同事自己擼的一套 DirectUI 系統,用 tinyxml 解析界面節點,項目簡單的時候沒啥,隨著ui越來越複雜,數千個節點,每個xml節點若干屬性,每個屬性就是一個字元串,我記得好像有500+ KB的 xml要解析,而且這部分界面還沒法延遲初始化,必須啟動載入時做完,啟動十分慢。profile下來,很多時間卡在 tinyxml上,整個過程接近 3秒,費時最前的操作卡在處理各種字元串的操作上。
把 tinyxml 換成其他 xml庫 ?沒那麼容易,項目各處模塊都在依賴 tinyxml的各種介面和類。一開始覺得內部的 TiXmlString 實現有問題,換成 std::string,vc 2012下時間從3秒增加到4秒,更不靠譜(vs2012應該已經有所謂SSO了),所以人家 tinyxml 這裡用自己的 TiXmlString 肯定也是比較過的,不然幹嘛不用 std::string 。
但問題總得解決,所以還得優化字元串實現:
1. 自己重新給 TiXmlString 實現了一套新的 SSO ,因為 xml裡面很多小字元串,10個位元組以內的佔比很多,這部分用 TiXmlString 裡面一塊靜態空間存儲,隨著capacity變化,超過限制長度的字元串才會開闢新的空間存儲,這樣避免了大量的內存分配,和碎片,總解析時間從3秒下降到 2秒。
2. 還是嫌慢,又把 tinyxml 繼續改寫,增加把文本 xml編譯成二進位格式的功能,平時開發用xml,實際發布用二進位版本的 xml,免去整個文本解析過程,時間進一步從2秒縮短到 0.8 秒。
3. 還嫌不夠快,接著改進二進位 xml文件結構,掃描整個 xml裡面用到的所有字元串,統一做一個字元串常量表放在文件最前面,這樣,二進位 xml文件里涉及到字元串的地方從緣來一段內存變成字元串表的一個索引 int,整個 TiXmlString 也變成對字元串常量表裡某個索引的引用,這樣徹底避免了字元串分配和維護操作,而且總內存變小了,比如 "type", "button", "label" "text" 等高頻字元串只存儲一遍,以前 1000個 "text" 字元串要解析1000遍,還要創建分配 1000次內存,有了常量表以後,所有的 "text" 都是一個引用,不需要1000便解析,更不需要1000次構造,時間從 0.8秒繼續下降到 0.2秒。
運營常識,客戶端項目,啟動時間直接和用戶流失率成正比,tinyxml字元串優化,前後把優化前的 3秒下降到優化後的 0.2秒,基本xml解析不再是一個瓶頸。
為什麼不同的庫要實現不同的字元串呢?從這個小例子可見一斑。
後來呢?嗯,後來有一天我不能忍了,我把整個項目的 gui系統弄成 Qt 的了,ui描述文件直接編譯成代碼,再也不用煩這些事情。
在結合看樓上高票說的 QString,可以感受下。
所以大家才會說:人在做,天在看,信 Qt,保平安。。。。。std::string並沒有實現字元串應該做的事,而僅僅只是用STL風格的容器類介面封裝了一下char[]罷了,其他std::xxstring同理。
與其說它是個字元串,還不如說它是個處理二進位數據的buffer,基本等同於std::vector&- 基礎單元不是char,是QChar,儲存的是utf-16字元。即,內部統一編碼為unicode,再無編碼之累。
- 任何常用字元串輸入,不管std::string、std::u16string、std::u32string、char*、wchar_t、char32_t、CFString、NSString等,都有對應的介面轉換為QString,並且轉換時需指定編碼(可以用latin1、utf8或者local8bit,local8bit一般是系統本地編碼,比如windows下的ansi)。
- 編碼轉換方面,採用單一職責原則,通過專門的QTextCodec類進行。但有特例——Unicode和latin1之間的轉換,是用asm/mips/sse寫的。
- 支持從QChar*的rawData指針構造字元串,此時不分配內部存儲,而是直接使用rawData指針作為數據內容。QChar大小為16bit,所以可以直接從雙位元組字元串指針強轉為QChar*。
- QChar提供了海量的字元操作方法,如isDigit、isLower、isSpace、isMark、isPrint等。
- 提供了split、left、right、mid、chop等等各種分割方法。
- 提供了contains、find、indexof等字元串搜索方法。
- 提供toUpper、toLower、trimmed、simplified、repeat等格式化方法。
- 由QString的方法切割、搜索等生成的字串,是由QStringRef構成的,共享原字元串的內存,在做出修改操作時才會實際拷貝過去。
- QString的拷貝構造是隱式共享,通過引用計數共享同樣的內部成員,在進行非const操作時才會觸發深拷貝。因此可以放心的到處亂傳,不用擔心拷貝開銷。
- 同樣支持容器類應有的正向/反向迭代器,以及reserve和squeeze(即shrink)。
- 性能上么……可以下個Qt查看下QString的源碼,注意要下Qt5的。源碼里的私有函數,幾乎全是用asm/mips/sse寫的。
- 提供完美的格式化輸出方式。不是sprintf這種落後的,無編譯校驗的寫法,而是QString("%2 %1 %2 %3).arg(1).arg(3.14).arg("hello world")這樣的,arg方法會依次替換%1到%99的對象,比如這裡輸出就是"3.14 1 3.14 hello world"。arg方法可以附帶更多的參數,用來指定整數進位、位數、填充字元、浮點表示格式、浮點位數……
- 提供QString tr(QString str)函數,和本地化框架對接,可以把輸入的字元串翻譯為當前語言的目標字元串。翻譯來源為外部載入的翻譯文件,若無對應則保留原始內容。
- QString.arg()這種可以通過 %1 %2 到 %99 任意對替換參數排序的機制,也是Qt Linguist本地化框架的核心。在編寫多語言版本的翻譯文件時,可以任意替換關鍵字的順序,從而完美滿足不同語言的語法順序。比如形為"%1 the %2"的英文字元串,在中文模式下翻譯為"%2%1",於是同樣是str.arg(name).arg(title),三個字元串拼接之後,在英文模式下是"Geralt the witcher",在中文模式下就是"狩魔獵人傑洛特「。
- 提供QStringLiteral(str)宏,可以在編譯期把代碼里的常量字元串str直接構造為QString對象,於是運行時就不需要構造開銷。
- QString並沒有使用sso,因為隱式共享內存佔用更少,拷貝構造性能也更好。至於單對象,不產生拷貝構造時的性能么,就需要一波benchmark了。
- 內部存儲是char*。
- 同樣具有和std::string類似的簡單的字元串操作介面。
- 同時也有QString的各種豐富的分割、搜索、格式化介面。
- 不限定編碼,和QString互相轉換時需指明編碼。
- 具有toHex/fromHex方法,可以把二進位數據轉換為可視化的十六進位數,如" "轉換為"00"。
- 具有PercentEncoding轉換介面,將特殊字元轉義為%形式,從而生成URI/URL風格的字元串。
- 具有base64轉換介面。
- 提供qCompress/qUncompress全局函數,通過zlib演算法對QByteArray進行壓縮/解壓。
- 提供qCheckSum全局函數,用來計算CRC-16。
- 同樣提供類似QString的,迭代器、reserve、squeeze等容器類介面。
- 同樣具有fromRawData介面,通過char*源碼直接構造QByteArray——和C API混合編程的神器,可以在不需要額外分配內存的情況下,直接把char*的字元串或者buffer封裝為QByteArray,從而可以使用最靈活的高級封裝來直接處理最底層的二進位數據,這方面也就C#的linq可以更勝一籌了。
- 同樣通過隱式共享降低參數傳遞開銷。
- QTextCodec提供字元串編碼轉換。
- QLocale提供本地化功能,見下:
- 提供目標區域的日期時間格式轉換。
- 提供目標區域的貨幣格式轉換。
- 提供目標區域的數字格式轉換(比如每三位一個逗號)。
- 提供度量衡轉換。
以上各個類(QString、QByteArray、QLocale等),每個類的源文件也就三五個,全都包含在QtCore模塊中。該模塊的relase版dll只有4M,並且可以通過編譯選項裁剪到1M級別。
而QtCore中的其他類,也都是如斯強大的,比如處理URI的QUrl,比如c++17才有的std::any(QVariant),比如IPC所用的QSharedMemory,比如說逐線程數據存儲QThreadStorage,比如c++17才有的filesystem,比如元對象系統和信號槽機制,比如xml流式處理,比如json處理(有人和rapidJson做過對比,大約為rapidjson耗時的1.4倍),比如定時器,比如DateTime……QtCore.dll,4M大小,涵蓋了c++11到17,除了network之外的所有東西,並且全都比標準庫中的更加強大,就問你是不是物超所值?好吧我偏題了。
總之,相比起來,std::string是什麼渣渣?我還從沒見過有比QString更強力的字元串類,尤其是加上QByteArray、QLocale這些相關類之後。(動態語言,利用更高級的語言特性,可以比QString更順手,但不會有太大差距,主要是類似linq的語法上的優勢,在功能性上並沒有差距,而QString的性能則遠高於前者。只有正則上QString是弱項——或者說整個c++在正則上都是弱項,因為動態語言可以把正則JIT到機器碼……)附:
QJson vs RapidJson vs JSON.parse 該文章里處理的json文件只有5kb。如果是大文件,則還需要考慮雙方使用的容器類的性能差異。在不專門特化allocator的情況下,Qt和STL的容器類基本不存在性能差距——論Qt容器與STL追加:Benchmark來啦——std::string/QString/QByteArray - 知乎專欄
因為標準庫的string是個弱雞。
真實世界的程序里的字元串,有這些問題:
- 多種編碼。
- 變長編碼。
- 一些特定關鍵字可能非常高頻。
然而C++標準庫的string,對這些事情全都沒有處理。
- 沒有編碼的概念。
- 沒有變長的概念,幾乎就是個char vector或者wchar vector。
- 常亮引用什麼的放棄治療,完全依賴C++的引用語義。
這讓人怎麼用?!!
一個原因是 std::string 在 1998 年才正式成為 C++(即 C++98 標準)的一部分,而此時距離這個叫 C++ 的語言問世已經有 15 年之久了。
- MFC 誕生於 1992 年
- wxWidgets 誕生於 1992 年
- Qt 誕生於 1995 年
總不見得去用一個不存在的東西是吧?
很多開源庫實現的時候,是面向特定需求的,會和其他的功能融合在一起,用std::string就覺得不爽,介面不爽,內存管理不爽,性能不爽,跨線程釋放不爽,線程不安全不爽,各種不爽,於是就自己寫吧,想怎麼寫怎麼寫,而且自己寫的還可以和其他輪子無縫結合,一看就是一個系統的。比如CEF就弄了個CefString,比如Qt弄個QString,比如MFC里有CString,比如tinyxml 里有 TiXmlString ,都是這樣子。
我自己也寫過 string 類,對此深有體會:你有一種需求不能被滿足時,就會覺得這個 std::string 真扯,連這點小需求都滿足不了,乾脆自己搞個得了。
還不是因為stl的string表現太過垃圾,整個stl中,最讓人扼腕嘆息之處莫過於string和iostream,偏偏這對冤家有糾纏在一塊,分都分不開。好了,我們回到string吧,從兩方面評價,性能和易用性。
鑒於string的應用太過廣泛,除了c++,嗯,c就不談,基本上所有的語言都把string當成內建的基本類型,並且還是immutable類型,並且只有一種字元串編碼,後面的語言,基本上都用utf8作為其內部編碼,如果你要處理其他字元編碼的字元串,比如說,ansi,gb2312,utf16GE,utf16LE,對不起,你都必須先將待處理的字元串轉換成utf8的string,操作完畢,再將其轉換回去,這個過程,轉來轉去,好像聽起來有點性能上的損耗,但,其實不要緊,真的不要緊,只是就是有時候使用上有些不方便。既然,語言層面上有且只有一種string,並且還是官方的,那麼,就算有種種不滿,或者有時候有些不方便,但是碼農也認命了,不認命又能如何,string這種東西,除了c++,沒有一種語言會提供底層機制和抽象設施,讓你重新做一個string。當然,基本上只要是官方的string(包括stl的垃圾string),很多時候,使用起來還是很無可厚非的。。而c++作為現代化的高級語言,完全就打破了這種慣例,每一條都打破了,還破的很徹底。string可以不是immutable,可以有多重編碼類型的string,而且好像沒有一種string內部採用utf8編碼,官方的string又非常垃圾。表面上,c++的string好像可以處理多種字元編碼,但是,除了最簡單的asc ansi編碼,沒有一種編碼能做的讓人滿意。
為什麼c++就沒有提供內建的string類型呢,基本原因,還不是因為string裡面有不定長度的字元緩衝,這就涉及到動態內存管理了,而這一塊,由於c++中沒有gc,並且,內存管理的手段又出奇的豐富多彩,顯然,c++官方不能也沒法給string指定唯一的內存管理方式。所以,無論如何,string都不可能在c++中是內建的類型,內建類型都是那些值類型的弱雞,好比int,char等等內存大小固定的良善子民,c++對這些本質上屬於值類型的數據抽象,最是再拿手不過,要抽象有抽象,要性能有性能,綜合評分上,在所有的語言中,應該是高居榜首的。那麼,c++對此,它的慣例就是,既然我提供feature很吃力不討好,那麼我就提供製造這種feature的feature,你們用戶自行去造輪子,有時候,stl庫上也會給出這種非語言feature的實現範例。
操,好像扯遠了,我們還是回到stl的string的指責上吧。
在c++中做數據抽象,套用lisp猿猴的一句話,lisp什麼都關心,除了性能;而c++猿猴恰好相反,除了性能,其他的什麼都不關心,我只在意性能。為了性能,當然,你不可能每次使用string的地方,包括參數傳遞,都把string內部的緩衝都整個複製過去的,string中做了很多很多該做不該做的抽象,什麼引用計數,什麼短字元優化,當然,這些性能優化的抽象,沒有免費的代價,都或多或少存在這樣那樣的問題,在此就不提了,總之一句話,有時候,老夫很不滿意啊,對外面群魔亂舞的種種string,stl的各種版本實現的string,qstring,cstring,都很鄙視。其實,很多情況下,我們只需要一個immutable的string啊,比如,要用string的一部分,比如,提取一下string左邊或者右邊的部分,並且這些臨時string的生命周期都比原來的那個string要短,好了,每次這樣用的時候,你媽的string就給我複製出來一個新的string,這當然性能上堪憂,不管你怎麼引用計數,都不可能優化這一塊的操作。一直折騰到現在,c++社區的那幫老頭子終於明白,原來c++真的很需要immutable的string啊,那就是string_view,題外話,如果一開始就有string_view的抽象,stl的string裡面的各種各樣的重載函數,為了性能上的重載,至少可以砍掉一半以上,很多性能上的鬼把戲抽象都可以去掉。這幫老傢伙似乎也明白到,allocator也很可能必須是polymorphism的,哈哈。哦,對了,在polymorphism時代的allocator,內存優化上的手段就可以交給allocator來做啦,屆時,短字元優化的伎倆也可以去見鬼了,討厭這些非zero overhead的抽象,這麼說吧,c++中,一切非zero overhead的抽象,到後面,總是要被證明是錯誤的,要被時代的潮流拋棄,包括c++本身。
顯然,字元編碼應該是造string輪子時候,必須慎重考慮的抽象,是抽象的大頭。但是,各種c++的string,在此問題上,都欠缺必要的考慮。最基本的事實就是,string本身應該提供這樣的介面,當然,緩衝字元的長度是必須要有的,也就是其內存表示,比如,gb2312編碼的字元串,"美國NBA",其緩衝字元長度是,每個漢字佔用兩個位元組,英文一個位元組,總共是緩衝字元長度是7,但是,字元串字元意義上的長度也要有啊,脫離其具體的存儲方式,就拿"美國NBA"來說,顯示,才不管漢字字元在內存裡面怎麼存儲,但是,就是要每個漢字都當作是一個字元,也就是,"美國NBA"的字元長度是5,然後,你string也要提供可以字元意義上的操作方式,這必須的嘛。緩衝長度和字元長度的不統一,確實很讓人心煩,但是,既然需求這樣子,那麼,就要實現啊。在utf32編碼中,緩衝字元佔4個位元組,完全就可以存儲一個字面字元,緩衝長度和字元長度完全就一一對應了,這顯然很方便,但是,內存佔用也太浪費了,很簡單的ansi編碼,到utf32下面,就要足足佔據4倍的空間。
好了,我們來看看stl的string,typedef basic_string&
其實,仔細思考,就會感受到,編碼方式裡面就已經定義了緩衝字元的表示方式以及相關的一切操作方式,也就是說,basic_string&
那麼,這,可以做到嗎?可以,我們統計一下,目前也就那麼幾種編碼方式,ansi每個字元一個位元組,locale相關的多位元組編碼,utf8編碼,ucs2編碼,utf16LE,utf16GE,utf32,來來求求就這麼幾種,有enum表示,或者為了可能的擴展性或者靈活性,用int也行。好比,我現在造的string輪子,這樣子,
enum class CharCode : char { Utf8, Utf16, Gb2312, //locale相關多位元組,暫時叫gb2312 Ansi, }; template& template&
講道理要求高性能環境下才不怎麼需要自己實現std::string吧,我都拿來當二進位塊用的。字元串操作全靠c lib和手寫。這種情況下std::string比那些給我亂轉編碼、亂搞對齊的string方便並且性能高不知道哪去了。(對我說的就是QString之類)
我來說句無關的C++不光開源每個都有自己的string,他媽的商業軟體的開發包也都有自己的string。用C++做項目第一件事情是啥?寫一個自己的string類啊!!!
只能說,string博大精深,千變萬化,針對特定的使用都有特定的優化處理來提高效率。所以,自己寫string成了每個開源庫的標配。stl的string不是不堪,在stl這個大環境下,string的設計都是有它的道理的,為了迎合STL的容器。
因為不好用啊,所以自己實現…
split、find、replace、strip、join、toupper、tolower……啥都沒有,怎麼玩啊
說過好多次了,string跟vector&
7、8年前,自己工程裡面關於字元串,也犯過選擇困難症。
首先,MFC/WTL的CString,依賴MultiByte/Unicode的設置。
在流行Unicode環境下,想保存成utf8文件或做網路通訊轉成MultiByte,就沒有現成類可用。然後選擇用STL::string,甚至有陣子用wstring來替換CString。
但是沒有Format、Find、Replace之類超級簡便寫法的現成函數,用起來很不爽。最後忍無可忍,自擼一個CStringA、CStringW,(實際是抄了WTL::CString).
除了繼承CString的所有優缺點之外,比如到window字元串的強轉,實現win32API直接調用,比如上面提到的更像樣的Format等古董函數。還內置utf8和unicode互轉,toInt,toFloat,toBase64等常用函數。一調用,世界一下子變得超級美好。補充一點:ABI的需要。
其實ABI中最麻煩的就是傳遞sized-buffer,其他的都還好說。如果你用STL中的容器來傳,那麼就要面臨不同runtime的兼容性問題。這種頻繁出現在參數輸入和結果輸出位置的基礎類型,要不就全用標準庫,要不全用自己的,沒有中間路線,否則各種轉換場景煩死人。
於是大家就都自己寫了。
一部分原因是因為歷史。C++98定案很晚,而很多庫在定案之前就出現了。另外,具體到實踐中,有很多混亂的問題,比如從ANSI/DBCS/MBCS到Unicode的轉變,等等。而且,C++的string也不完美。所以,到處都有自行設計的string類。
不是不堪一擊,而是STL 容器的作用就是容器而已,而UI 庫里的string 則是為了方便處理響應的功能。如果統一編碼的話,那就不適合做STL 的容器了。畢竟STL 追求的是效率,你想想連一串英文全都用 UTF-16 來表示,這還是c ++么?
我偶爾寫c++, 標準輸入輸出我用stdio.h,字元串我用char*和string.h(strcpy strcmp strcat memcpy malloc .....) 內存管理我用malloc free , 好像stl里沒什麼常用的了,對於我這個不是專門寫c++的。
C++搞了那麼多特性也不就是為了能實現一個在別的語言裡面內置的字元串嗎(雖然依舊有各種問題),所以,你不用C++寫個字元串類,對得起語言的設計者嗎?
推薦閱讀:
※C++或QT項目如何進行CI(Continuous Integration)?
※Clang 解析錯誤和報錯的機制?
※Visual studio中的「添加引用」是什麼意思?
※C++中的數組與指針的一個小疑惑?
※C++解析xml有什麼好用的輪子?