最終更新日時:
が更新

履歴 編集

ジェネリックコンポーネントにおける例外安全性

C++標準ライブラリのために規定した例外安全性の経験から学んだこと

概要

この文書は実世界の必要性に対する応答の中で蓄積された知識を表す: つまり、 C++ 標準テンプレートライブラリは、役立つ、そして明確な例外との相互作用を示し、 エラー捕捉の機構は C++ 言語の中核に組み込まれている。 この文書では、例外安全性の意味を探求し、例外と汎用性についての驚くべき神話を明らかにし、 プログラムの正当性を理由付けるための価値ある道具について述べ、 例外安全性を実証するための自動化されたテストの手続きを概説する。

Keywords: exception-safety, exceptions, STL, C++

1 例外安全性とは何か

簡単に言えば、コンポーネントの例外安全性とは、 実行中に例外が投げられたとき、正当な振る舞いを示すことを意味する。 ほとんどの人にとって、「正当な」という用語は、エラー捕捉に対する通常の例外全てを含む: つまり、資源は漏れるべきではないし、プログラムは実行を継続できるように、 明確な状態であり続けるべきである。多くのコンポーネントにとって、 これはまた、エラーに出会った時に呼び出し元に報告されるような例外も含む。

より公式に言えば、コンポーネントの中から例外が投げられたときに、 もしその不変性が損なわれないなら、そのコンポーネントは最小限の例外安全性を持っていると言える。 後に見るが、通常、少なくとも3種類の異なる例外安全性が区別されている。 これらの区別は巨大なシステムの振る舞いについて記述し、理由付けるのに役立つのである。

汎用コンポーネントでは、通常我々は、さらに、 例外中立性 という期待をもつ。 これは、コンポーネントの型パラメータによって投げられた例外は、 そのコンポーネントの呼び出し元まで、変わることなく伝えられるべきである、 ということを意味する。

2 神話と迷信

例外安全性は、これまでのところ簡単なものに見える: それは、より伝統的なエラー捕捉の技術を使ったコードに期待する以上のことを、 何も構築したりはしない。しかし、心理学的な観点からこの用語を調べてみることは、価値があるだろう。 C++ が例外を持つ以前は、誰も「エラー安全性」について語らなかった。

まるで例外は、正しいコードに対する ミステリアスな攻撃 であり、 我々が自らをその攻撃から守らなければいけないようなものであると見なされているかのようである。 言うまでもなく、これはエラー捕捉との健全な関係に繋がらない! 標準化の間、変更に対する幅広いサポートを要求する民主的な過程で、 私は広く支持された多くの迷信に出会った。 汎用コンポーネントにおける例外安全性の議論を始めるために、 それらのいくつかを見ておくことは意味のあることだろう。

「テンプレートと例外の相互作用は、良く理解できない。」 これら両方が言語の新しい特徴であると考える人々から良く聞かれるこの神話は、 簡単に却下できる: そこには、相互作用はないのである。 テンプレートは、一度実体化されれば、全ての面で通常のクラスや関数と同じように機能する。 例外と関連したテンプレートの振る舞いを考えるための単純な方法は、 そのテンプレートの特別版の実体化がどう機能するかについて考えることである。 最後に、テンプレートの汎用性は、何も特別なことを引き起こさない。 コンポーネントのクライアントは操作の一部を提供するが(この操作は、もし特別版でないなら、 任意の例外を投げるだろう)、我々が良く親しんだ仮想関数や、 単純な関数ポインタを使った操作についても、同じことなのである。

「例外安全性をもつ汎用コンテナを書くのが不可能であることは、 良く知られている」 この主張は、Tom Cargill による文書、 [^4] に関連してよく聞かれる。そこで彼は、汎用スタックテンプレートに対する例外安全性の問題について探求している。 彼の文書で、 Cargill は多くの役立つ問題を掲げているが、 残念ながらそれらの問題に対する解決法を提供できていない。彼は解決は不可能である、と提案して結論付けている。不幸にも彼の文書は、 多くの人に、そのような空論の 「証拠」 として読まれてしまった。 しかしこの文書が出版されてから、例外安全な汎用コンテナの多くの実例があったのである。 C++ 標準ライブラリコンテナもその中にはいる。

「例外を扱うとコードは遅くなり、テンプレートは本質的に 可能な限りのパフォーマンスを得るために使われる。」 C++ の優れた実装は、例外が投げられるまでにその例外を扱うひとつの命令サイクルを費やすことはしないで、 例外は関数呼び出しの同じようなスピードで捕捉可能である [^7]。 それだけで、例外を使ったプログラムに、 エラーの可能性を無視したプログラムと同等のパフォーマンスを提供している。 例外を使うと実際は、結果的に別の理由で「伝統的な」エラー捕捉の方法よりも早くなる。 まず、 catch 節はコンパイラに、どのコードがエラー捕捉に費やされるかを明確に示す。 このため、通常の実行パスから分離することが可能であり、参照の局所性が改善する。 次に、「伝統的な」エラー捕捉を使ったコードは典型的に、単一の関数を呼び出した後、いつも返り値を検査しなければならない。 例外を使えば、このオーバヘッドは完全に消える。

「例外はプログラムの振る舞いを推論することを難しくしてしまう」 通常、この神話が支持されて引用されるのは、 スタック巻き戻しの間に「隠れた」実行パスを通る、という意味においてである。 隠れた実行パスはローカル変数が関数から戻る前に破棄されることを期待している C++ プログラマにとっては、なにも新しいものではない。

ErrorCode f( int& result )         // 1 
{                                  // 2 
    X x;                           // 3 
    ErrorCode err = x.g( result ); // 4 
    if ( err != kNoError )         // 5 
        return err;                // 6 
    // ...More code here... 
    return kNoError;               // 7 
}

上の例では、6行目と7行目に X::~X の「隠れた」呼び出しがある。 そう考えれば、例外を使うことで、エラー捕捉に対して、明白なコードの無駄は存在しない。

int f()                 // 1 
{                       // 2 
    X x;                // 3 
    int result = x.g(); // 4 
    // ...More code here... 
    return result;      // 5 
} 

例外をより良く知っている多くのプログラマにとって、2番目の例は実際は最初の例よりも、 読みやすく理解しやすい。 「隠れた」コードパスは同じくローカル変数のデストラクタを呼び出している。 更にこれは、冷害が起こった場合にそれぞれの関数呼び出しの後に、 潜在的なリターン文があるかのような、正確に振る舞う単純なパターンに従うのである。 通常の実行パスはエラー捕捉によって分かりにくくならないので、可読性は向上し、 返り値は自然なやり方で使えるように解放されるのである。 例外が正確さを向上することが出来る、さらに重要なやり方がある: それは、 単純なクラス不変性を可能にすることによる。 最初の例では、もし X のコンストラクタが資源を確保する必要があるなら、 失敗を報告する手だてはない: C++ ではコンストラクタは返り値を持てないからである。 例外を使わない場合は通常、資源を要求するクラスが構築の仕事を完了する、 別の初期化関数を含まなければならない、という結果になる。 プログラマはそのため、クラス X のオブジェクトが使われるとき、 完全な X を手にしたのか、それともどこかで構築に失敗したものを手にしたのか、 決して確信をもてない(或いは更に悪いことに、誰かが単に初期化関数を呼び忘れたのかもしれない。)

3 例外安全性の契約的原則

非汎用的なコンポーネントは例外安全として記述することが出来るが、 汎用的なコンポーネントの場合は、クライアントによる構造化が可能なので、 例外安全性は通常、コンポーネントとクライアントの契約に依存する。 例えば、汎用的コンポーネントの設計者はコンポーネントのデストラクタで使われる演算が、 どんな例外も投げないことを要求するだろう。汎用的コンポーネントはその代わり、次の保証のうちのひとつを提供するだろう。

  • 基本的保証 : コンポーネントの不変性は保持され、資源漏れはない。
  • 強い保証 : 演算は成功して完了するか、例外を投げるかのどちらかである。 例外を投げる場合、プログラムの状態は演算が始まる前の状態と全く同じである。
  • 例外不送出保証 : 演算は例外を投げない。

基本的保証 は、全てのコンポーネントに負わせることの出来る、 例外安全性に対する単純で最小限の基準である。 例外の後でもまだコンポーネントは以前のように使うことが出来ると述べているに過ぎない。 重要なことだが、不変性の保持によって、スタック巻き戻しの一部として潜在的に、 コンポーネントを破棄することが出来るのである。 この保証は実際には、見た目ほど役立つものではない。 もしコンポーネントが多くの有効状態を持つなら、例外の後にコンポーネントの状態が、 またはその状態だけが有効な状態なのかどうか知ることは出来ない。 この場合、回復のための選択肢は限られている: コンポーネントの破棄か、 さらに使う前に、ある既知の状態にコンポーネントをリセットするかである。 次の例を考えてみよう:

template <class X> 
void print_random_sequence() 
{ 
    std::vector<X> v(10); // A vector of 10 items 
    try { 
        // Provides only the basic guarantee 
        v.insert( v.begin(), X() ); 
    } 
    catch(...) {} // ignore any exceptions above 
    // print the vector's contents 
    std::cout "(" << v.size() << ") "; 
    std::copy( v.begin(), v.end(), 
    std::ostream_iterator<X>( std::cout, " " ) ); 
} 

我々はみんな、例外後に v が有効であることを知っているので、 この関数は X のどんなランダムなシーケンスでも出力することが出来る。これはクラッシュしないと言う意味で「安全」ではあるが、出力の内容は予想できないものである。

強い保証 は、「責任を持つか、巻き戻すか」という意味論を完全に提供する。 C++ 標準コンテナの場合、これは例えばもし例外が投げられても全てのイテレータは有効なままであることを意味する。 我々はまた、コンテナが例外が投げられる前と全く同じ要素を持っていることも知っている。 失敗したら、なんの効果も及ぼさない処理は、明らかに利点がある: 例外が発生した場合でも、 プログラムの状態は単純で予測可能なのである。 C++ 標準ライブラリでは、ノードを使うコンテナ、list, set, multiset, map, multimap のほとんど全ての演算が強い保証を提供している。)

例外不送出保証 は最も強いものであり、演算は例外を投げないことを保証されている、 というものである: これは常に成功して完了する。 この保証はほとんどのデストラクタにとって必要なものであり、 C++ 標準ライブラリコンポーネントのデストラクタは実際にすべて、 例外を投げないことを保証されている。 例外不送出保証、他の理由で重要となることを我々は見るだろう。

4 法的論争

契約がより複雑になることは避けられない: その代わり整理することが可能である。 C++ 標準ライブラリには、任意の型引数にひとつの保証を与え、 例外を投げないというクライアント型からの約束が追加されれれば、より強い保証を与えているものもある。 例えば、標準コンテナ操作 vector<T>::erase はどんな T にも 基本的保証 を与えるが、コピーコンストラクタとコピー代入演算子が例外を投げないなら、 例外不送出保証 を与えている。

5 コンポーネントはどのレベルの例外安全性を規定する必要があるか

クライアントの観点から、可能な限り最も強いレベルの安全性が理想的である。 もちろん 例外不送出保証 は多くの演算に対して不可能であるが、 強い保証についてはどうだろうか? 例えば、 vector<T>::insert に対してちょっとした振る舞いが欲しいと仮定しよう。 vector の中間への挿入は、新しい要素のための場所を作るために、 挿入点以降の要素を、次の位置にコピーする必要がある。 もし要素のコピーが失敗に終われば、操作の巻き戻しは既に行われたコピーの「取り消し」を必要とする。

ひとつの可能な選択肢は、新しい配列の内容を、毎回新しいメモリで構築して、 成功したときのみ古い内容を破棄するように insert を再定義することである。 残念ながら、このアプローチにはコストがかかる: vector の終端付近での挿入は、 以前ではほとんどコピーを行う必要がなかったが、 このアプローチでは全ての要素をコピーしなければいけない。 基本的保証 はこの操作に対する「自然な」水準の安全性である。 パフォーマンスを脅かすことなく保証を与えているのである。 実際ライブラリの全ての演算は、層のような「自然な」水準の安全性を提供している。

パフォーマンスの要求は基準の草案の中では、確立した部分であり、 パフォーマンスは STL の基本的な目的であるので、 これらの要求の中で提供されうる、より強い安全性を明記する試みは為されなかった。 全てのライブラリが 強い保証 を与えているわけではないが、 基本的保証 を提供する、標準コンテナほとんどの演算は、上に述べた、 「新たなコピーを作る」という戦略を使うことで、強い保証を持つことが出来る。

template <class Container, class BasicOp> 
void MakeOperationStrong( Container& c, const BasicOp& op ) 
{ 
    Container tmp(c); // Copy c 
    op(tmp); // Work on the copy 
    c.swap(tmp); // Cannot fail
}

この技は、より強い保証を提供する(そして異なるパフォーマンス特性を提供する)、 似たようなコンテナを作るためのラッパクラスに導入することが出来る。

6 私たちは、私たちが得ることができる全てを得るべきか

特殊な実装を考えれば、安全性についての自然な水準を判断することを望めるだろう。 コンポーネントに対する要求を確立するのにこれを使うことで、 実装が制限されるという危険性が生じる。 我々が使いたくなるような、より効率的な実装を誰かが作り上げても、 それが我々の持つ例外安全性への要求を満たしていないことに気づくかもしれない。 STL が扱っているデータ構造とアルゴリズムという、よく研究された領域では、 このようなことに誰も期待を寄せないかもしれないが、 それでも実際、より優れたものが作られている。最近の introsort アルゴリズムは、その良い例である [^6]。 これは、既に確立した quicksort に対して、最悪の計算量を必要とするような場合での、 大幅な改善を示している。 実際に標準コンポーネントの要求がどの程度のものなのかを決定するために、 実世界の典型的な場合を考えた。 選ばれたテストケースは 「合成コンテナ」である。 2つ以上の標準コンテナの合成である、そのコンテナは広く必要とされているだけでなく、 巨大なシステムで不変性を維持することについての単純で代表的な事例である。

// SearchableStack - A stack which can be efficiently searched 
// for any value. 
template <class T> 
class SearchableStack 
{ 
 public: 
    void push(const T& t);           // O(log n) 
    void pop(); // O(log n) 
    bool contains(const T& t) const; // O(log n) 
    const T& top() const;            // O(1) 
 private: 
    std::set<T> set_impl; 
    std::list<std::set<T>::iterator> list_impl; 
}; 

ここでは、listset のイテレータのスタックとして振る舞う: 全ての要素は最初に set に入り、その結果その位置が list に入れられる。 不変性というのは、簡単なことである: setlist は常に、 同じ要素数を持ち、set 全ての要素は list の要素により参照されている、ということである。 以下の push 関数の実装は、 setlist によって提供される自然な水準の例外安全性で、 強い保証 を提供するために設計されたものである。

template <class T>                                // 1
void SearchableStack<T>::push(const T& t)         // 2
{                                                 // 3
    set<T>::iterator i = set_impl.insert(t);      // 4
    try                                           // 5
    {                                             // 6
        list_impl.push_back(i);                   // 7
    }                                             // 8
    catch(...)                                    // 9
    {                                             // 10
        set_impl.erase(i);                        // 11
        throw;                                    // 12
    }                                             // 13
}                                                 // 14

このコードは実際に、ライブラリの何を要求するだろうか? 非 const 演算が行われる行を調べてみる必要がある。

  • 4行目: 挿入が失敗したが、 set_implがその過程で変更されている場合、 不変性は保たれない。そこで、set<T>::insert からの 強い保証 に頼ることが出来なければならない。
  • 7行目: 同様に push_back が失敗して list_impl がその過程で変更されているなら、不変性は保たれない。 そこで、list<T>::insert からの 強い保証 に頼ることが出来なければならない。
  • 11行目: ここで4行目の挿入を「巻き戻し」ている。もしこの操作が失敗すれば、 不変性を回復することは出来ないだろう。結局 set<T>::erase からの 例外不送出保証 に頼ることになる。
  • 11行目: 同じ理由で、 ierase 関数に渡すことが出来るということにも、 頼らなければならない: set<T>::iterator からの、 例外不送出保証 に頼る必要があるのである。

私は標準化の際にこの方法で問題を扱うことで、多くを学んだ。 まず、合成コンテナに対して明示された保証は、実際はそのコンポーネントからのより強い保証 (11行目の 例外不送出保証 )に依存するということである。 また、この単純な例を実装するために、自然な水準の例外安全性を全ての面で利用した。 そして、この分析は、以前私が、演算をそれぞれ独立したものと考えていたときには見逃していた、 イテレータへの要求を明らかにした。 結論は、可能な限り自然な水準の例外安全性を提供すべきだ、ということである。 より速く、しかしより安全ではない実装は常に、標準コンポーネントの拡張として提供されるべきである。

7 例外安全性の自動テスト

標準化の過程の一部として、私は STL での例外安全な参照の実装を作った。 エラー捕捉コードは実際にかなり厳密にテストされたが、 エラー状態を起こすことが難しいので、そのテストは部分的である。 初めて実行されたときにクラッシュしたエラー捕捉コードを考えるというのは、 とても一般的である-出荷される製品では。実装が実際に宣伝通りに動くという確信を強めるために、 私は自動化されたテストスイートを設計した。これは同僚の Matt Arnold の精緻な技術に基づいている。

テストプログラムは基本的なところから始まった: 特にグローバル演算子 newdeleteについての強化と計測である。 出来る限り多くの潜在的な問題を明らかにするために選ばれた型引数で、 コンポーネント(コンテナとアルゴリズム)の実体が作られた。 例えば、全ての型引数にはヒープ領域にメモリを割り当てられるポインタが与えられた。 そのため、コンテナに格納されたオブジェクトをリークさせることは、 メモリリークとして検出された。

最後に、ポインタが間違って示す可能性のある場合に、その都度、 演算に例外を投げさせることが出来るような仕組みを設計した。 クライアントが提供し、例外を投げることが許されている全ての演算の最初に、 ThisCanThrow の呼び出しが加えられた。 ThisCanThrow の呼び出しはまた、 テストされる汎用的演算が例外を投げるかもしれない全ての場所にも加えられた。 例えば、より機能を強化したものに置き換えられた、グローバル演算子 new である。

// Use this as a type parameter, e.g. vector<TestClass> 
struct TestClass 
{ 
    TestClass( int v = 0 ) 
        : p( ThisCanThrow(), new int( v ) ) {} 
    TestClass( const TestClass& rhs ) 
        : p( ThisCanThrow(), new int( *rhs.p ) ) {} 
    const TestClass& operator=( const TestClass& rhs ) 
        { ThisCanThrow(); *p = *rhs.p; } 
    bool operator==( const TestClass& rhs ) 
        { ThisCanThrow(); return *p == *rhs.p; } 
    ...etc... 
    ~TestClass() { delete p; } 
};

ThisCanThrow は単に、「throw カウンタ」をデクリメントするだけであり、 もしそれが 0 になったら、例外を投げる。 テスト毎に、外側のループで、徐々に大きな値にカウンターを設定して開始し、 演算のテストが完了するまで繰り返し試す、という形を取った。 結果は、失敗する可能性がある実行パスに沿って、連続的なステップで、オペレーションが例外を投げた。 例えば、 強い保証 をテストするのに使われた関数の単純なバージョンがある:

extern int gThrowCounter; // The throw counter
void ThisCanThrow() 
{ 
    if (gThrowCounter-- == 0) 
        throw 0; 
} 

template <class Value, class Operation> 
void StrongCheck(const Value& v, const Operation& op) 
{ 
    bool succeeded = false; 
    for (long nextThrowCount = 0; !succeeded; ++nextThrowCount) 
    { 
        Value duplicate = v; 
        try 
        { 
            gThrowCounter = nextThrowCount; 
            op( duplicate ); // Try the operation 
            succeeded = true; 
        } 
        catch(...) // Catch all exceptions 
        { 
            bool unchanged = duplicate == v; // Test strong guarantee 
            assert( unchanged ); 
        } 
        // Specialize as desired for each container type, to check 
        // integrity. For example, size() == distance(begin(),end()) 
        CheckInvariant(v); // Check any invariant 
    } 
}

注意すべきは、この種のテストは非汎用的なコンポーネントより、汎用的なものの方が 遙かに簡単で、煩わしさのないものである、ということである。 これは、テストに特化した型引数を、 テストされるコンポーネントのソースコードを変更することなく使うことが出来るからである。 また上の StrongCheck のような汎用関数が広範な値と演算のテストを行うのに役立つ。

8 さらにくわしく知るための資料

私が知る限り、現在 STL の利用可能な例外安全性には2種類の記述しかない。 STL の例外安全の実装のリファレンスでの、オリジナルの仕様 [^2] は、非公式な仕様であり、単純で自明(そして冗長)である。 そこでは、この文書で概説してきた、基本的と強い保証の区別が使われている。 それは明らかに、資源漏れを禁止していて、最終的な C++ 標準と比べ、多くの面で同じなのだが、 保証という点では実質的に異なる。 私はこの文書の改訂版が速やかに作られることを望んでいる。 C++ 標準での例外安全性の記述[^1] はほんの少しだけ公式なものであるが、読みにくい「規格化」 で構成されていて、ウェブ上ではほとんど見ることが出来ない。とくに、資源漏れについては直接は全く扱われていない。 それが規格であるという利点を持っているに過ぎない。

例外安全の実装[^5] に関するオリジナルのリファレンスは、SGI STL の古いバージョンである。 これは限られた能力の C++ コンパイラのために設計された。 これは完全な STL の実装ではないが、コードは読みやすいし、 役立つ基底クラスのテクニックを、コンストラクタでの例外捕捉をなくすために説明している。 参照の実装を検証するために使われた完全なテストスイート[^3] は、引き続き SGI STL の最近のバージョン全てで使われている。 そして他のベンダの実装をテスト(通らなかった)するのにも使われている。 文書で注記されているように、それは隠れたコンパイラのバグを明らかにするのに、特に最適化と例外捕捉コードが相互作用するような場所では、強力であるだろう。

参考文献

  1. International Standard ISO/IEC 14882, Information Technology-Programming Languages-C++, Document Number ISO/IEC 14882-1998, available from http://webstore.ansi.org/ansidocstore/default.asp.
  2. D. Abrahams, Exception Safety in STLport, available at http://www.stlport.org/doc/exception_safety.html.
  3. D. Abrahams and B. Fomitchev, Exception Handling Test Suite, available at http://www.stlport.org/doc/eh_testsuite.html.
  4. Tom Cargill, "Exception Handling: A False Sense of Security," C++ Report, Nov-Dec 1994, also available at http://www.awl.com/cp/mec++-cargill.html.
  5. B. Fomitchev, Adapted SGI STL Version 1.0, with exception handling code by D. Abrahams, available at http://www.metabyte.com/~fbp/stl/old.html.
  6. D. R. Musser, "Introspective Sorting and Selection Algorithms," Software-Practice and Experience 27(8):983-993, 1997.
  7. Bjarne Stroustrup, The Design And Evolution of C++. Addison Wesley, Reading, MA, 1995, ISBN 0-201-54330-3, Section 16.9.1.

脚注

[^1]: おそらく Cargill の場合、解決に対する最も大きな障害は、 彼が、不幸な組み合わせの選択をしてしまったということであった: 彼がコンテナのために選んだインタフェースは、彼が要求する安全性の特徴と一致しないものだったのだ。 どちらかを変更すれば、彼は問題を解決できただろう。

[^2]: C++ でデストラクタから例外が投げられることは通常進められない。 デストラクタは、それ自身他の例外によって引き起こされるスタック巻き戻しの途中で呼び出されるかもしれないからである。 2番目の例外がデストラクタを越えて伝えられることが可能なら、 問題はすぐに解決する。

[^3]: 実践としては、 この関数はあまりにも貧弱なランダムシーケンス製作器である!

[^4]: 変更操作を行うアルゴリズムが通常、 強い保証 を提供できないことは、 注目に値する: ある範囲の変更された要素を巻き戻すために、 例外を投げるかもしれない operator= を使って、以前の状態に戻さなければならないのである。 C++ 標準ライブラリでは、この規則はほとんど守られていて、 巻き戻しの振る舞いは破棄だけで成立している: 例外は、uninitialized_copy, uninitialized_fill, uninitialized_fill_n

[^5]: C++ 標準ライブラリのクライアントが提供する全ての型引数は、 デストラクタが例外を投げないことを要求される。 その代わりに、 C++ 標準ライブラリの全てのコンポーネントは少なくとも 基本的保証 を提供するのである。

[^6]: 変更操作を行う多くのアルゴリズムに対して C++ 規格では 似たような整理が為されている。しかし規格化の過程で時間の制約は全く考えられていない。

[^7]: 要素の Compare がコピー時に例外を投げるかもしれないような連想コンテナは、この技を使っていない。 スワップ関数が失敗するかもしれないからである。

[^8]: これは、たびたび望まれ、しかしまだ見知らぬ container_traits<> テンプレートのもう一つの潜在的な利用を示している。 例外安全性の制約を満たす、コンテナの自動選択である。

[^9]: set<T> に対する要求を減らし、 例外時の問題を減らすために、 erasetry/catch ブロックに入れようとするかもしれない。 しかし結局、問題なのである。 まず、erase は失敗し、この場合必要な結果を産み出すための実行可能な代替案は存在しない。 また、より一般には、型引数が多様なので、汎用コンポーネントにたいして、 どんな選択肢も成功する保証はめったになされるものではない。

[^10]: STL の設計の有力な哲学は、全ての利用にとって基本的でない機能は、その機能が必要なときに、基本のコンポーネントを適応することで得られる限り、 効率を求めないでいく、ということである。 これはそのような哲学に端を発しているが、 基本的保証 でさえ、 既にその保証を持っていない基本のコンポーネントを適応して、そのような保証を得ることは難しいか、不可能である。

[^11]: メモリシステムをどのようにして守るかについての素晴らしい議論が、 次のものに書かれている: Steve Maguire, Writing Solid Code, Microsoft Press, Redmond, WA, 1993, ISBN 1-55615- 551-4.

[^12]: この技は、テストされる操作が例外中立であることを必要とすることに注意すること。 もし操作が例外から回復して、処理を続けようとするなら、throw カウンタは負の値になるだろう。 そしてその後の失敗するかもしれない操作は、例外安全性に対してテストされない。

[^13]: 例外安全性を導入した規格草案に対する変更は、 変更される単語の数が多いという理由だけで修正が拒否されるような、 草案作成の過程のかなり遅い時期に為された。 不幸にも、この結果、簡潔さを求める余り、ある程度妥協したものとなった。 Greg Colvin はこれらの変更の範囲を最小化するために必要な、 賢い言語-法実務についての責任がある。