C++ Learning

第17回 std::string_view ― コピーしない軽量文字列

C++17 で入った コピー 0 の文字列参照型std::string / C の char* / 文字列リテラルのどれからでも作れ、それらを統一的に扱う関数引数として最適。SSO の境を超える大きな文字列を多用するコードで劇的な高速化が狙えます。ただし寿命の落とし穴が 1 つあるので、そこだけ覚えれば使いこなせます。

このページで押さえること
✅ 最低限ここだけ覚える
  • 読み取り専用の文字列参照型
  • std::string / char* / リテラルから作れる
  • 関数引数の const std::string& を置き換える候補
  • 元の文字列より長生きしてはいけない
⭐ 余裕があれば読む
  • 中身は ポインタ + 長さ だけ
  • substr / find / remove_prefix
  • ヌル終端されていないことがある
  • メンバに持つのは原則 NG

1. まず触ってみる ― string_view は「覗き穴」

std::string_view は、既にある文字列データをコピーせず覗くための型です。「読むだけ・書き換えない」用途限定で、関数の引数で劇的に役立ちます

イメージは本の栞(しおり)。本(std::string や文字列リテラル)はそのままで、栞だけ作って「ここからここまで」と印を付ける。栞そのものは軽いので、関数から関数へ渡し回しても速い。

▶ string_view で覗く範囲を変える
元の文字列:
start = , length =

まずは 3 行だけ

first_sv.cppC++ 最小例
#include <string_view> void print(std::string_view s) { // ← コピーしない std::cout << s << "\n"; } print("hello"); // リテラルから OK std::string s = "world"; print(s); // string から OK const char* p = "!"; print(p); // char* から OK
ここまでで覚えること(2 つ):
  • 読むだけなら、関数引数に std::string_view を使う(const std::string& の置き換え)
  • 元の文字列が生きている間だけ有効。長生きさせない

よくある素朴な疑問

Q. const std::string& と何が違う?
const std::string&std::string しか受け取れません。C のリテラル "hello" を渡すと暗黙に std::string が作られてコピーコストがかかります。std::string_view ならリテラルも char* もコピーなしで受け取れます。

Q. 書き換えできない?
できません。 string_view は常に読み取り専用(const)。書き換えたいなら std::string を使う。

Q. いつ使うか?
関数の引数が一番の使い道。ローカル変数やメンバ変数に使うのは落とし穴が多いので避けます(§5 で)。

2. string_view の中身はポインタ+長さだけ

std::string_view の内部は、実はたった 2 つのメンバしかありません:

合計 16 バイト(64-bit 環境)。std::string の 24〜32 バイトより小さく、何よりヒープ確保がないのが利点。

string_view の中身構造
// std::string_view は概念的に struct string_view { const char* data_; size_t size_; // メソッドを大量に持つ (substr, find, ...) }; // 元の文字列はコピーされない // ただ「ここから何文字」と記録するだけ
string との比較サイズ感
// std::string (libstdc++ 実装の典型) struct string { char* data_; // ヒープへのポインタ size_t size_; size_t cap_; char sso_buf[16]; // SSO 用 }; // string は自前でメモリを所有 // string_view は他人のメモリを借りる

パフォーマンスのイメージ

大量の関数呼び出しで、文字列リテラルを引数として渡すとき:

const std::string&
100% (毎回 string を作る)
std::string_view
8%
※ リテラルから呼び出した 100 万回のベンチマーク(概算)

3. 関数引数として使う

関数引数として使うと、その関数の引数が柔軟になるのが一番のメリットです。

従来型ごとに OW
// 文字列リテラルも string も受けたいので // どちらかを選ぶしかない void greet(const std::string& name); // ↑ リテラル渡すたびに string 作成 // あるいは両方オーバーロード…面倒 void greet(const char* name); void greet(const std::string& name);
string_view統一
void greet(std::string_view name) { std::cout << "Hi, " << name; } // これ 1 つで全パターン対応(コピー 0) greet("Alice"); // リテラル OK std::string s = "Bob"; greet(s); // string OK const char* p = "Carol"; greet(p); // char* OK
判断フロー:
  • 関数で文字列を読むだけstd::string_view(推奨)
  • 関数で文字列を書き換えるstd::string&
  • 関数で中身のコピーを保持したい(メンバに入れる等) → std::string で値渡し

4. 基本操作(string と似ている)

string_view のメソッドは std::string読み取り系だけを取り出したものです。

よく使う読み取り
std::string_view sv = "Hello, World!"; sv.size(); // 13 sv.empty(); // false sv[0]; // 'H' sv.front(); // 'H' sv.back(); // '!' sv.find("World"); // 7 sv.substr(7, 5); // "World"(新しい view を返す)
view 専用ポインタ移動
std::string_view sv = " padded "; sv.remove_prefix(2); // 先頭 2 文字を『無視』 // sv = "padded "(先頭ポインタをずらすだけ、コピーなし) sv.remove_suffix(2); // 末尾 2 文字を『無視』 // sv = "padded"

string_view から string への変換

view は他人のメモリを借りているだけなので、長生きさせたいなら自分でコピー(= string に変換)します:

view → stringコピーを作る
std::string_view sv = get_data(); // 明示的にコピーを作る(推奨) std::string s{sv}; // 暗黙変換はされない(コピーは意図的に書く) // std::string s = sv; ← これはコンパイルエラー
ここまでで string_view の日常は OK
残りは寿命の落とし穴。ここは必ず理解しておいてください。view を誤用するとクラッシュの原因に。

5. 寿命の落とし穴

string_view は「他人のメモリを覗く窓」です。その他人が先に死ぬと、窓から覗いている先は無効になります。これは典型的なダングリング(dangling)参照の一種。

罠 1: 関数の戻り値で view を返さない

NGダングリング
std::string_view bad() { std::string s = "hello"; return s; // ← s は関数終了で破棄 } // view は無効なメモリを指す auto sv = bad(); std::cout << sv; // 未定義動作
OKstring を返す
std::string good() { return "hello"; // ← 値ごと返す } auto s = good(); std::cout << s; // OK

罠 2: 一時オブジェクトから view を作る

NG即座にダングリング
std::string_view sv = make_string(); // make_string() の戻り値は一時オブジェクト // このあと即座に破棄される → sv は無効 std::cout << sv; // 未定義動作
OK変数を経由
std::string s = make_string(); std::string_view sv = s; // s が生きている間 sv は有効 std::cout << sv; // OK

罠 3: クラスのメンバに string_view を持つ

NG寿命追跡が困難
class User { std::string_view name_; // ← 危険 public: User(std::string_view n) : name_(n) {} // name_ が指す文字列がいつまで生きるか // クラス利用側が気をつけないといけない };
OKstring で所有
class User { std::string name_; // ← 自分で所有 public: User(std::string_view n) : name_(n) {} // クラス自身が name をコピーして持つので安全 };
原則:
  • 関数の引数に使う → ◎ (一番の適所)
  • ローカル変数に一時的に使う → ◯(元の string が同じスコープにいる場合)
  • 関数の戻り値に使う → △(戻り値の元が長生きするなら OK)
  • クラスのメンバにする → ×(string を使う)
広告スペース

確認クイズ

string_view を 4 問で確認。

Q1. std::string_view が含むデータとして正しいのは?

文字列全体のコピー
SSO 用のスタックバッファと長さ
ポインタと長さだけ
ヒープへのポインタとキャパシティ
string_view は 2 つのメンバ(const char* と size_t)だけを持つ軽量な型。文字列データそのものは他人(std::string や リテラル)に所有させ、自分は「ここから何文字」という情報だけ持ちます。

Q2. 関数で文字列を読むだけ使う場合、モダン C++ で最も良い引数の型は?

std::string
std::string&
const std::string&
std::string_view
C++17 以降、読み取り専用の文字列引数は std::string_view が最適。string / char* / リテラルすべてをコピーなしで受け取れます。const std::string& だとリテラルから呼び出した時に毎回 string が作られ、コピーコストがかかります。

Q3. 次のコードのうち未定義動作になるのは?

std::string s = "hi"; std::string_view sv = s; // 使う
std::string_view sv = get_string(); // 戻り値は一時オブジェクト
std::string_view sv = "literal"; // リテラル
void f(std::string_view s); f("hello"); // 関数呼び出し
②では get_string() が返す一時オブジェクトはその式の終了時に破棄されるので、sv はダングリングな view に。他の 3 つは元の文字列が sv の寿命より長く生きるので OK。リテラルはプログラム終了まで生きるので一番安全。

Q4. string_view のメンバにないものは?

size()
substr()
push_back()
find()
string_view は読み取り専用なので、書き換え系(push_back, +=, erase, replace など)はすべて存在しません。size / find / substr / remove_prefix / remove_suffix など読み取り系だけ持ちます。