跳到主要內容

C++17 新功能 - string_view

C++17 新功能 - string_view

string_view

使用 std::string 來做字串解析的好處是可以利用其各式各樣的功能,像是 substrfind 等,缺點則是 std::string 實作設計成 value semantic,複製與賦值都隱含 heap 操作 ,即使是 C++11 有了 move semantic 得以避免複製,還是得付出解構的成本。為了解決這個問題,C++14 加入了 std::experimental::string_view ,這個類別可以基於 char const * 或是 std::string 建構,提供唯讀的參照到原本的記憶體。舉例如下

#include <experimental/string_view>
#include <assert>

using namespace std;
using namespace std::experimental;

int main() {
  char const *data = "asdf";
  string_view sv(data);

  assert(data == sv.data()); // 記憶體位置相同
  sv[0] = 'b';   // compiler error (string_view 是唯讀)
}

因為是唯讀參照,不需要任何 heap 上的操作,可以節省相當的成本。類似功能在 C++14 之前,許多函式庫就已經提供類似的功能,例如 re2::StringPiece 、boost::string_ref、llvm::stringref 等。這次在 C++17 看來沒太多爭議就納入標準函式庫成為 std::string_view

實例運用

使用 std::string 實作的簡易字串分割

輸入:“a,b,c,d”
輸出:
a
b
c
d

#include <string>

using namespace std;

struct tokenizer {
  tokenizer(string const &s) : m_s(s), m_beg(0), m_end(0) {}
  
  string operator()() {
    for(;m_end < m_s.size();++m_end) {
      if (m_s[m_end] == ',') {
        auto res = m_s.substr(m_beg, m_end - m_beg); // heap access
        m_beg = ++m_end; 
        return res; // heap access
      }
    }
    if (m_end <= m_s.size())
      return m_s.substr(m_beg, m_end); // heap access
  }
  bool more() const { return m_end < m_s.size(); }
private:
  string const &m_s;
  size_t m_beg, m_end;
};

void gendata(string &data, size_t count) {
  for(size_t i=0; i < count; ++i) {
    data.push_back('a'+ i%26);
    if (i+1 != count)
      data.push_back(',');
  }
}

int main() {
  string data;

  gendata(data, 1<<23);
  tokenizer tokr(data);
  
  while(tokr.more()) {
    cout << tokr() << endl;  
  }

  return 0;
}

使用 string_view 實作的簡易字串分割

因為手邊的 compiler (gcc-4.9) 只有支援到 C++14,因此 string_view 還是位在 experimental 命名空間內

只需要增加標頭、修改 tokenizer 有用到 string 之處 (注解標示處):

#include <experimental/string_view>
using namespace std::experimental;


struct tokenizer {
  tokenizer(string_view s) // change to string_view
    : m_s(s), m_beg(0), m_end(0) {}
    
  string_view operator()() {  // string_view as return type
    for(;m_end < m_s.size();++m_end) {
      if (m_s[m_end] == ',') {
        auto res = m_s.substr(m_beg, m_end - m_beg);
        m_beg = ++m_end;
        return res;
      }
    }
    if (m_end <= m_s.size())
      return m_s.substr(m_beg, m_end);
  }
  bool more() const { return m_end < m_s.size(); }
private:
  string_view m_s;  // change to string_view
  size_t m_beg, m_end;
};

效能比較

效能的數據是單獨量測下面這個迴圈耗費的時間

string token; // and string_view token
while(tokr.more()) {
  token = tokr();  
}

資料量為 16M ,兩個程式各跑 10 次,測試平台是 cygwin、Windows7、16G RAM。
時間單位為 millisecond。

string string_view
2523 22
2517 22
2518 22
2510 23
2513 22
2512 22
2507 22
2510 24
2516 21
2524 22

數據有點驚人,實際上拿 string 的版本到 Linux 上面跑大概只需要一半的時間,string_view 的版本由於 toolchain 還沒準備好,之後有機會再改到其他平台測試補上數據。

更新數據 (Ubuntu, clang-4.0)

string string_view
557 19
526 19
533 19
511 19
532 18
611 21
512 20
499 20
513 23
510 19

Compilers Support Status

Vendor Flag Version
GCC –std=c++1z 7.0.0
GCC –std=c++17 7.0.0
Clang –std=c++1z 3.9.0
Clang –std=c++17 5.0.0

Written with StackEdit.

留言

這個網誌中的熱門文章

得利油漆色卡編碼方式

得利油漆色卡編碼方式 類似 Munsell 色彩系統 ,編碼方式為 HUE LRV/CHROMA 例如 10GY 61/449 ( 色卡 ) 編碼數值 描述 10GY hue ,色輪上從 Y(ellow) 到 G(reen) 區分為 0 ~ 99 ,數值越小越靠近 Y,越大越靠近 G 61 LRV (Light Reflectance Value) 塗料反射光源的比率,數值從 0% ~ 100% ,越高越亮,反之越暗,也可理解為明度 449 chroma 可理解為彩度,數值沒有上限,越高顏色純度 (濃度) 越高 取決於測量儀器,對應至 RGB 並不保證視覺感受相同。 參考資料: 色卡對照網站 e-paint.co.uk Written with StackEdit .

UTF8 與 Unicode 的轉換 (C++)

UTF8 與 Unicode 的轉換 (C++) 先釐清一下這兩者的性質 Unicode: 為世界上所有的文字系統制訂的標準,基本上就是給每個字(letter)一個編號 UTF-8: 為 unicode 的編號制定一個數位編碼方法 UTF-8 是一個長度介於 1~6 byte 的編碼,將 unicode 編號 (code point) 分為六個區間如下表 1 Bits First code point Last code point Bytes Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 7 U+0000 U+007F 1 0xxxxxxx 11 U+0080 U+07FF 2 110xxxxx 10xxxxxx 16 U+0800 U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx 21 U+10000 U+1FFFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 26 U+200000 U+3FFFFFF 5 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 31 U+4000000 U+7FFFFFFF 6 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 觀察上面的表應該可以發現 除了 7 bits 的區間外,第一個 byte 開頭連續 1 的個數就是長度,例如 110XXXXX 就是 2 byte 長,而 1110xxxx 就是 3 byte 除了第一個 byte 外,之後的 byte 前兩個 bit 一定是 10 開頭,這樣的好處在於確立了編碼的 self-synchronizeing,意即當編碼為多個 byte 時,任取一個 byte 無法正常解碼。 Note 第一點中的例外 (7 bits) 是為了與 ASCII 的相容性,而第二點會影響到 code point 至 UTF-8 的轉換。 為了與 UTF-16 的相容性,在 R

C++17 新功能 try_emplace

C++17 新功能 try_emplace 回顧 emplace 大家的好朋友 Standard Template Library (STL) 容器提供如 push_back , insert 等介面,讓我們塞東西進去; C++11 之後,新增了 emplace 系列的介面,如 std::vector::emplace_back , std::map::emplace 等,差異在於 emplace 是在容器內 in-place 直接建構新元素,而不像 push_back 在傳遞參數前建構,下面用實例來說明: struct Value { // ctor1 Value ( int size ) : array ( new char [ size ] ) , size ( size ) { printf ( "ctor1: %d\n" , size ) ; } // ctor2 Value ( const Value & v ) : array ( new char [ v . size ] ) , size ( v . size ) { printf ( "ctor2: %d\n" , size ) ; memcpy ( array . get ( ) , v . array . get ( ) , size ) ; } private : std :: unique_ptr < char [ ] > array ; int size = 0 ; } ; struct Value 定義了自訂建構子 (ctor1),以指定大小 size 配置陣列,複製建構子 (ctor2) 則會配置與來源相同大小及內容的陣列,為了方便觀察加了一些 printf 。當我們如下使用 std::vector::push_back 時 std :: vector < Value > v ; v . push_back ( Value ( 2048 ) ) ; 首先 Value 會先呼叫 ctor1,傳給 push_ba