この記事は陳腐化しました。C++11 から Copy on Write は実装禁止になりました。g++4.5 以降でも消えました。但し、終端文字を必要とする仕様は健在です。
(2013/08/25 追記)
std::stringを参照渡しにすると高速化した。何を言っているのか(ry
なんて記事を見ると改めてstringの扱いはハマるとこ多いよなあと感じる。結論から言うと可能な限りconst&やiterator-pairを使えばいいんですが。
stringは一つの文字列プールを指す文字列クラスで、vectorと次の点で似ています。
- 連続したアドレス上に保管される
- メモリは自動で管理される(大抵は一定量から倍々)
- 要素に’\0’(NULL文字)を使用可能
但し、その実装は処理系によって結構違います。細かいところまでは読んでいませんが、大まかにvcとgccの実装の特徴を紹介します。
g++4.3.4
実装の特徴は、
- 格納する文字列に必要なプールは常にアロケータから確保
- プールにCopy on Write(CoW)の実装
stringのコピーの際に文字列プールを共有(参照カウント)して不要な複製をコストを削減しています。以下捕捉です。
- sizeof(string)がプールへのポインタ一つ分なので引数の受け渡しなどで有利
- 非constの添字アクセスはほんのちょっと余計な処理が入るかも
- 空文字列は専用のプールが用意されており領域の確保は起きない
- 文字列定数は必ずコピーされる
- 常に終端文字が入っている
実際に変更しなくても、非constで要素にアクセスしたときプール全体のコピーが発生するので、コピーを回避するにはconst必須です。
VC2010
実装の特徴は、
- 終端文字を含めて16B以下はstringのインスタンス内に入る
- それ以上はアロケータから確保
16B以下(15文字/wstringなら7文字)の文字列の生成や結合が高速です。短い文字列を大量に扱う場合特に効果を発揮します。以下捕捉です。
- sizeof(string)は16B+3ポインタ分と大きめ(Debugビルドでは+1ポインタ)
- 文字列定数は必ずコピーされる
- 常に終端文字が入っている
主観的な意見
コピーを回避したいならconst&やshared_ptrを使うので、CoWは余計なお節介だと思います。しかもゲームなど応答性が必要な場合、遅延実行の発動タイミングを意識する必要あります。というか長い文字列を扱う場合は別のクラスを使った方が無難です。個人的にはメモリプールの管理と文字列ポリシーを分けてアダプタで繋ぐような実装が欲しいです。
どちらも常に終端文字が入っていますが、本来C形式である必要はなく、それ故data()とc_str()が用意されています。Cと親和性が必要でない限りc_str()は使わなくていいので、特にvcで16文字格納できないのはちょっと勿体無く感じます。またCの関数にdata()を突っ込んでも動くので誤解される原因でもあります。実際は仕様上の制約でデメリットが大きいため終端文字を付けない実装は見たことがありませんが。
ついでに検査用のソースも載せておきます。でも細かい実装は読んだ方が分かり易いです。
#include <iostream> #include <string> using namespace std; int main() { string s1, s2 = "a"; cout << "size of string: " << sizeof(s1) << endl; cout << "initial capacity: " << s1.capacity() << endl; while ((char*)&s1 <= s1.data() && s1.data() < (char*)(&s1 + 1)) s1 += s2; cout << "capacity inside of string: " << (int)s1.size()-1 << endl; s1 = s2; cout << "copy on write: " << (s1.data() == s2.data()) << endl; return 0; }