C++17: string_view、map 與異質比較查詢

身為一個 C++ 程序員,我都會盡我所能避免不必要的計算。所以當我聽到 C++ 17 新增了 std::string_view 類別,我就迫不及待的想要使用它。std::string_view 的概念是:你可以指稱另一個 std::string 的一個子字串,但是不複製這段子字串。例如:

std::string s("xxx yyy zz");
std::string_view sv(s);
std::string_view x(sv.substr(0, 3));  // x == "xxx"
std::string_view y(sv.substr(4, 3));  // y == "yyy"
std::string_view z(sv.substr(8, 2));  // z == "zz"

一個典型的使用方法是用來實作字串分割的函式:

std::vector<std::string_view> tokenize(std::string_view sv) {
  static const char delims[] = " \t";

  std::vector<std::string_view> res;
  std::string::size_type pos = 0;
  while (true) {
    pos = sv.find_first_not_of(delims, pos);
    if (pos == std::string::npos) {
      break;
    }
    std::string::size_type end = sv.find_first_of(delims, pos + 1);
    if (end == std::string::npos) {
      res.push_back(sv.substr(pos));
      break;
    }
    res.push_back(sv.substr(pos, end - pos));
    pos = end + 1;
  }
  return res;
}

不過值得一提的是:使用 std::string_view 的時候必需注意他的指稱的 std::string 的生命週期。如果被 std::string_view 指稱的 std::string 物件先被解構了,繼續使用 std::string_view 就可能會有未定義結果。

接下來稍微轉換一下話題。因為 std::string_view 對指稱的 std::string 物件的生命週期有額外的要求,所以在一般情境下,使用 std::stringstd::map 的索引型別 (Key Type) 會是比較好的選擇:

std::map<std::string, int> m{{"x", 1}, {"y", 2}, {"z", 3}};

但這會衍生出一個問題:我們在呼叫 std::map::find() 的時候,只能傳 std::string。如果我們直接輸入以下程式,我們會得到編譯錯誤:

void test(std::string_view k) {
  auto &&it = m.find(k);  // XXX: compile error

  // ...
}

這個錯誤的成因是容器的索引型別和 find() 的參數型別不一致。一個簡單但不好的解決方式是把 std::string_view 明確地轉換成 std::string

void test(std::string_view k) {
  auto &&it = m.find(std::string(k));  // explicit cast

  // ...
}

這個方法會引入非必要的浪費。上面這段程式碼會建構一個暫時的 std::string 物件,用這個 std::string 物件當索引,再解構這個暫時物件。而且這個暫時物件會複製 std::string_view 的「內容」,這正是我們想要避免的。

更好的解法是使用 C++ 14 新增的「異質比較查詢 (Heterogeneous Comparison Lookup)」。要使用異質比較查詢,容器的比較函式物件 (Comparator) 必須定義 is_transparent 成員型別。在 functional 標頭檔裡面的 std::less<>(不帶參數)就剛好符合這個要求:

#include <functional>

std::map<std::string, int, std::less<>> m2{
    {"x", 1}, {"y", 2}, {"z", 3}};

void test(std::string_view k) {
  auto &&it = m2.find(k);  // OK

  // ...
}

std::less<> 雖然很好用,但是他非常的自由。以上面的例子來說,m2.find() 可以接收任何可以和 std::string 比較的物件。如果我們不想要有這樣的行為,我們也可以自己寫一個比較函式物件:

struct string_less {
  using is_transparent = void;

  bool operator()(const std::string &lhs,
                  const std::string &rhs) const {
    return lhs < rhs;
  }

  bool operator()(const std::string &lhs,
                  std::string_view rhs) const {
    return lhs < rhs;
  }

  bool operator()(std::string_view lhs,
                  const std::string &rhs) const {
    return lhs < rhs;
  }
};

希望這篇文章有清楚地介紹 std::string_view 和異質比較查詢。我們下次見囉。