2005年11月 9日

STL の string のリファレンスカウント

Effective STLによると、多くの STL の実装では string クラスの内部において、文字列のコピーを減らすために、リファレンスカウントが行われているそうです。そこで、手元の環境の string クラスでリファレンスカウントが実際に行われているか調べてみました。

 

結論としては、Debian GNU/Linux sarge + GCC 3.3.5 の環境では、リファレンスカウントが行われていました。次のコードをコンパイルして実行すると 2つの string オブジェクトの data() は同じアドレスを指している (つまり、メモリ上の同じ文字列を共有している) ことがわかります。

#include <stdio.h>
#include <string>
using namespace std;

int main() {
    string a = "foo";
    string b = a;

    printf("%p %p\n", a.data(), b.data());
    return 0;
}

手元での実行結果は次の通りです。

0x8049abc 0x8049abc

printf の前に a[0] = 'b'; のような行を加えると、2つの string オブジェクトの data() が指すアドレスは変わっています。

0x8049acc 0x8049abc

これは string::operator[] の呼び出した時点で文字列のコピーが行われるためです。 /usr/include/c++/3.3/bits/basic_string.h を見ると、非const の oeprator[] では _M_leak() という関数を最初に呼んで文字列のコピーとリファレンスカウントの更新などの処理を行っています。

      const_reference
      operator[] (size_type __pos) const
      { return _M_data()[__pos]; }

      reference
      operator[](size_type __pos)
      {
        _M_leak();
        return _M_data()[__pos];
      }

basic_string.h を見渡すと、非const の begin() や end() でも他のメンバ関数でも _M_leak() が呼ばれていることがわかります。非const のイテレータはオブジェクトの破壊的な操作に使われることがあるため、共有を解除する必要があります。

ところで、begin() の定義を見ると、次の 2つがあります。

      iterator
      begin()
      {
        _M_leak();
        return iterator(_M_data());
      }

      const_iterator
      begin() const
      { return const_iterator(_M_data()); }

プログラムで次のように書いた場合、どちらの begin()が呼ばれるでしょうか。

string s = "foo";
string::const_iterator i = s.begin();

一見、 const_iterator を返す後者の方が呼ばれるような気がしますが、実は iterator を返す前者の方が呼ばれます。これは 非const のオブジェクトが呼び出したメンバ関数に const 版と非 const 版があった場合、非const版が呼ばれるためです。 iterator から const_iterator へは暗黙的に変換されるため、問題は起きません。