C++03 の仕様から変更のあったライブラリ

この記事は C++11 Advent Calendar 13 日目の記事です。


C++03 で用意されていた標準ライブラリは基本的にそのまま残っていたり deprecate されていたりとかぐらいしかありませんが、それでも少し仕様が変更されていたりとかしています。
後方互換性を維持するためあまりダイナミックな変更はされていませんが、それでも結構嬉しい変更だったりするので、覚えてる部分だけ紹介しておきます。(誰か一覧とか作ってくれませんかね?)

アルゴリズム関数に渡す関数オブジェクトの制限の緩和

C++03 では、アルゴリズム関数(accumulate, inner_product, partial_sum, adjacent_difference)に渡す関数オブジェクトでは、一切の副作用が禁止されていました。そのため、

struct Hoge {
  int operator()(int,int) { return ++side_effect; }
  Hoge() : side_effect(0) { }
  int side_effect;
};

std::vector<int> v = ...;
int result = accumulate(v.begin(), v.end(), Hoge());

このコードは未定義動作になります。
このことについて、Effective STL 第37項では以下のように書かれています。

for_eachの関数パラメータには余分な作用が許されるがaccumulateの関数パラメータには余分な作用が許されないのはなぜか不思議に思うかもしれない。これはSTLの核心をつく質問である。寛大なる読者のみなさん、われわれの理解を超えている神秘はまだ存在する。なぜaccumulateとfor_eachに違いがあるのか。筆者を納得させるような説明はいまだ聞いたことがない。

Effective STL p157 第37項

ということで、納得させる説明が無かったんでしょう。C++11 では、アルゴリズム関数については、引数に渡された要素を書き換えることと、イテレータを無効にすることのみを禁止するようになりました。
なので、上記のようなコードは C++11 としては全く問題のない処理になります。

の関数に渡す関数オブジェクトに対する制限の緩和

C++11 の§25.1¶10に、以下のような Note が追加されました。

[Note: 関数オブジェクトを引数に取る for_each 以外のアルゴリズムは、その関数オブジェクトを自由にコピーしても構わない。そのため、アルゴリズムの利用者はそのことに注意する必要がある。コピーされてしまうことが問題である場合、reference_wrapper や同様の解決手段を使ってオブジェクトの中身をコピーしないようなラッパークラスを使うといった対策を行う必要がある。]

§25.1¶10

これがどれぐらい嬉しいのかは、 @Cryolite 先生がつぶやいていたので転載すると、

だそうです。完全転送とかよく分かってませんが、つまりそういうことらしいです。
追記1:
完全転送って何のことかと思ったら、reference_wrapper に operator()(Args...) が入ったようです。
だから↑のURLみたいに std::ref で渡しても Functor として扱えるってことですね。 std::reference_wrapper すごい。
追記2:
この仕様部分はNoteなので実際のところ効力を持たないのですが、ref使って回避しろって言ってくれてるのがありがたいのでみんな使うようにしましょう。

の in-place 関数に対するムーブセマンティクスの利用

C++11 ではムーブセマンティクスが導入されたので、効率の良いムーブを実装しているクラスをコンテナに入れておけば、
swap_ranges, iter_swap, remove, remove_if, unique, unique_if, reverse, rotate, partition,
sort, stable_sort, partial_sort, nth_element, inplace_merge,
push_heap, pop_heap, make_heap, sort_heap
あたりのアルゴリズム関数でその恩恵を受けることができます。
ただ、既存のクラスが既にstd::swapを特殊化している場合、Swappableを要求する処理は速くなりません。
それ以外の、例えば remove や unique なんかは Swappable を要求する処理ではないので、ムーブを実装すれば速くなる可能性は高いです。

basic_string のメモリの連続性の保証

C++03 では str[0] から str[N-1] までの領域(N は str.size())が連続していることは保証されていませんでしたが、C++11 では連続していることが保証されます。

string str("abc");
// C++11 ではこれが保証される
assert(&str[2] == &str[0] + 2);

basic_string の非 const な operator[] 呼び出しでのアクセス範囲の変更

C++03 でも C++11 でも、以下のコードは有効でした。

const string str("abc");
str[str.size()];

これは CharT() (std::string なら char() なので、つまり 0)の値を指す参照が返されます。


そして C++03 では、以下のコードは未定義動作でした。

string str("abc");
str[str.size()];

しかし C++11 では、このコードは有効で、同様に CharT() の値を指す参照が返されます。


ただし、非 const な参照が返されるからといって、この値を書き換えたりするのは未定義動作です。

string str("abc");
str[str.size()] = 'a'; // 未定義動作!


ちなみに str[N-1] と str[N] の領域が連続していることは(少なくとも直接は)保証していません。
なので、

assert(&str[str.size()] == &str[0] + str.size());
assert(str[str.size()] == *(&str[0] + str.size()));

これらの assert は失敗する可能性があります(2番目の assert は最悪無効な領域を dereference していることになる)。

basic_string の data と c_str の挙動の変更

C++03 では c_str メンバ関数の返す文字列は null 終端されていることが保証されていましたが、data メンバ関数が返す値は null 終端されている保証はありませんでした。
C++11 では、c_str も data も同じ仕様になり、両方とも null 終端されていることが保証されるようになりました。
また、C++03 では c_str, data の計算量を規定していませんでしたが、C++11 では定数時間であることが保証されることになりました。

basic_string のイテレータが無効になるタイミングの緩和

C++03 では、以下のタイミングでイテレータが無効になるとされていました。

  1. operator>>、getline、swap (メンバ関数含む)といった、非 const な basic_string を引数に取る関数に渡した場合
  2. data や c_str メンバ関数を呼び出した場合
  3. operator[], at, begin, rbegin, end, rend 以外の、非 const メンバ関数の呼び出し
  4. ↑のメンバ関数呼び出し(イテレータを返すバージョンの insert と erase は除く)に続いて、最初の非 const メンバ関数版の operator[], at, begin, rbegin, end, rend 関数の呼び出をした場合

4. の挙動は分かりにくいかもしれませんが、つまり、次のような挙動になります。

string s1("abc");
string s2("def");

string::iterator it1 = s1.begin();
s1[0]; // これは it1 を無効にしない

s1 = s2; // 3. の条件を満たす非 const メンバ関数(operator= 関数)の呼び出しなので it1 は無効になる

string::iterator it2 = s1.begin();
s1[0]; // 4. の条件を満たしているので it2 は無効になる

string::iterator it3 = s1.begin();
s1[0]; // これは 2 回目以降だから 4. の条件を満たさないので、it3 を無効にしない

この変わった挙動は、copy-on-write な実装を許可するためでした。
copy-on-write というのは、普段は文字列を共有しておき、書き込まれる時に共有している文字列をコピーして独立させ、書き込むという実装です。

string s1("abc");
string s2 = s1; // s1 と s2 は同じ文字列 "abc" を指す(という実装であるとする)

s2[0] = 'd'; // s2 は新しく "abc" という領域を作り、その文字列の 0 文字目を 'd' に変更する
             // s2 が領域を作り直しているので、この直前でイテレータを取っていた場合は、それが無効になる

assert(s1 == "abc"); // s2 は新しい別の領域を書き換えたので s1 の "abc" という文字列には影響しない

s2[0] = 'e'; // s2 は既に文字列を共有していないので、新しく領域を作ることは無い。
             // なのでこの直前でイテレータを取っていた場合でも、そのイテレータは無効にならない

もちろん copy-on-write な実装になっていなければならないということはありませんでしたが、そういう実装も許していたのです。


これが、C++11 では次のような仕様に変更されました。(取り消し線は削除された部分、赤文字は追加された部分)

  1. operator>>、getline、swap (メンバ関数含む)といった、非 const な basic_string を引数に取る関数に渡した場合
  2. data や c_str メンバ関数を呼び出した場合
  3. operator[], at, front, back, begin, rbegin, end, rend 以外の、非 const メンバ関数の呼び出し
  4. ↑のメンバ関数呼び出し(イテレータを返すバージョンの insert と erase は除く)に続いて、最初の非 const メンバ関数版の operator[], at, begin, rbegin, end, rend 関数の呼び出をした場合

この変更によって、std::string で copy-on-write な実装をするのは不可能になりました。
なぜそのようにしたのかというと、イテレータが無効になるタイミングを教えるのが難しかったのと、並列処理でパフォーマンスを発揮できず、また、その際にバグを埋め込む可能性が高かったからです。
詳細は Concurrency Modifications to Basic String を読んで下さい。
実際のところ、それらの問題があるため、最近のコンパイラでは copy-on-write な実装をしていないようなので、そんなに影響は無いようです(このことも上記のリンク先で書かれています)。