📺

文字列の参照はstd::string_view!

に公開

今回は C++17 で導入されたstd::string_viewを紹介しようと思います。名前通り文字列のビューということで、文字列の読み取り専用の型です。使い方としては、例えば以下のように文字列を関数に渡して出力したい時ですね。

void printStringView(std::string_view str) {
    std::cout << str << '\n';
}

std::string_viewはどの型の文字列からも作成できるのが特徴です。↑の関数には以下のように全型の文字列が渡せます。

int main() {
    std::string s{ "foo" };
    std::string_view sv { s };

    printStringView(s.c_str());     // C の文字列を渡す
    printStringView(s);             // std::string を渡す
    printStringView(sv);            // 他の std::string_view を渡す

    return 0;
}

std::string_viewは内部的には「文字列を参照するポインタ」と「文字数」を持つような実装になっていて、元の文字列のコピーが発生しないので、パフォーマンスが最適です。
しかも読み取り専用なので、安全に利用できます。

ここまでの説明を聞いて、「え?const std::string&で渡せばよくね?」と思ったそこのあなた!私も最初はそう思いました。
確かに、std::string_viewが導入される前は以下のようにどの型の文字列もstd::stringの参照型で渡せていました。

void printConstStringRef(const std::string& str) {
    std::cout << str << '\n';
}

上記のように3つの型の文字列を渡すとどうなるかを見てみましょう。

int main() {
    std::string s{ "foo" };
    std::string_view sv { s };

    printConstStringRef(s.c_str());         // C の文字列を渡す
    printConstStringRef(s);                 // std::string を渡す
    printConstStringRef(std::string(sv));   // 他の std::string_view を渡す

    return 0;
}
  • std::stringを渡すと、コピーが発生しないです。ここはstd::string_viewとパフォーマンスが変わらないです。
  • Cの文字列を渡すと、参照できるstd::string型の変数がないため、一時的な変数が作成され、文字列がコピーされます。
  • std::string_viewはそのまま渡せないせいで、明示的にstd::string型に変換してから渡さないといけないため、その変換で必然的にコピーが発生します。

std::string_viewの場合は、どのケースもコピーが発生しないのに、const std::string&の場合は、std::string以外のケースはコピーが発生します。このユースケースでは明らかにstd::string_viewの方がいいですね。

後、std::string_viewは読み取り専用なので、本体の中身は変えられないですが、substrremove_prefixremove_suffixというメンバー関数でその文字列の一部を抜粋することができます。

でも注意しないといけないのは、std::string_viewは参照とポインタと同じということです。以下のように本体の文字列がスコープから消えた後に使うと未定義動作になります。

std::string getName() {
    return "fooooooooooooooo";
}

int main() {
  std::string_view name { getName() };  // getName() の戻り値は一時オブジェクトでこの行の後に消えます。
  std::cout << name << '\n';            // 未定義動作

  return 0;
}

未定義動作ということで、↑のコードをいくつかのコンパイラで試してみたのですが、clangでもgccでも文字化けしていました。

さらに、std::string_viewを作ってから本体を変えたりすると、そのstd::string_viewが無効になるため、その後使う際も未定義動作になります。

int main() {
    std::string s { "fooooooooooooooo" };
    std::string_view sv { s };  // sv は s のビューとして作成される

    s = "bar";                  // s が変えられたことによって sv が無効になる
    std::cout << sv << '\n';    // 未定義動作
    return 0;
}

実行してみると、"bar" ではなく "baroooooooooooo" が出力されます。

でも一度無効になったstd::string_viewは以下のようにまた有効にできます。

int main() {
    std::string s { "foobar" };
    std::string_view sv { s };  // sv は s のビューとして作成される

    s = "bar";                  // s が変えられたことによって sv が無効になる
    sv = s;                     // sv に s が代入されるここで sv がまた有効になる
    std::cout << sv << '\n';    // ちゃんと「bar」が出力される

    return 0;
}

こちらを実行してみると、ちゃんと "bar" が出力されます。

纏めると、どの型の文字列でも読み取り専用で何かに渡したい時はstd::string_viewを使いましょう!


|cpp記事一覧へのリンク|

Discussion