第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 や文字列リテラル)はそのままで、栞だけ作って「ここからここまで」と印を付ける。栞そのものは軽いので、関数から関数へ渡し回しても速い。
まずは 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 つのメンバしかありません:
const char*(文字列データへのポインタ)
size_t(長さ)
合計 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 を作る)
※ リテラルから呼び出した 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 など読み取り系だけ持ちます。