More Exceptional C++ 読書ノート

2009 年の正月だったと思う。かの名著 Exceptional C++ の続編を秋葉原駅前ヨドバシの有隣堂書店で発見した。前作が「本物」であったがゆえに、本書も購入してみたのだが───。

著者:

Herb Sutter

訳者:

浜田真理

出版社:

ピアソン・エデュケーション

発行年:

2008 年

ISBN:

978-4-89471-483-0

序章

  • <古代ギリシャの哲学者ソクラテスは、問答によって弟子に伝授した> →ソクラテスメソッド

  • <標準 C++ と標準ライブラリの効果的な利用方法>

  • <合理的根拠>

  • 本書は <標準ライブラリの効果的な使用方法に重点を置いている> と繰り返す。

  • 読者に求める前提条件として、<古典的な名著> であるBjarne Stroustrup, “The C++ Programming Language” やら Stan Lippman and Josée Lajoie, “C++ Primer” を挙げている。これらの本は分厚くて、まともに読みこなせなかったことを思い出した。

  • <特に、私の管理下にないものは移動する。>

  • <インターネット社会における印刷メディアの過酷な状況>

  • 著者は本書レビュアーに対して <酷評を惜しみなく浴びせてくださいました> と礼を述べている。

汎用プログラミングと C++ 標準ライブラリ

前作にも同じテーマの章があった。

ストリームの操作

  • <basic_ostream が、basic_streambuf オブジェクトを入力として受け付ける operator<<() を提供> (p. 3) しているので、ここでの回答である cout << cin.rdbuf() の形を着想できる。

  • is_open() はこういう形でないと使わないな。

  • <均衡のとれた判断> <適切な妥協点> (p. 5)

  • <拡張性を考慮した設計がしばしばカプセル化を意味している> (p. 6)

  • <関連性の分離を適切に行う> (p. 6)

ステートフル述語

  • <一般に「ステートフル」であると言えば、変更可能な状態のことを意味する。したがって、ステートフル述語においては適用過程の順が重要になる> (p. 14)

  • <ステートフル述語とそれ以外の述語には重要な相違点がある> (p. 15) と切り出して、ステートフル述語のコピーがオリジナルと等価とならないものと、適用順が不定の実装であるような remove_if についての思考実験を始める。それで標準アルゴリズムの要件が厳密には足りていないよ、と著者は指摘したい。

継承と特性 (traits)

クラステンプレート traits について。

  • テンプレートの特殊化がキモ。

  • std::char_traits<T>

  • [C++98] 17.1.18

  • Requirements - 制約クラス

  • <B の意味がわからない限り、instantiated_type についてもわからない> (p. 35)

  • <空の基底クラス X_base は、typedef を提供するためだけに存在している。しかしながら、通常、派生クラスはそれを再度 typedef し直す。無駄ではないだろうか?> (p. 36)

  • 追記のコードをコンパイルしたいが、手許に環境がない。残念。

コンテナではないコンテナ

  • スタニスラウって誰だ。

  • <ポインタが無効化される時期にさえ注意すれば安全だ。それはまた、イテレータが無効化される時期でもある> (p. 39)

    vector<char> v;
    // ...
    char* p = &v[0];
    // ...
    
  • map<Name, PhoneNumber> の逆方向検索マップが map<PhoneNumber*, Name*, Deref> になるのが面白い。

  • 某書にもあるが <std::vector<bool> はその「どんなコンテナ」に含まれない> (p. 41)

  • <「プロキシコンテナ」とは、オブジェクトに対する直接的なアクセスや操作ができないコンテナのことである> (p. 42)

  • <vector<bool> のアクセスは遅い> (p. 43)

  • <vector<bool> は、プロキシコンテナの実装例を示すことが目的の一部であり、ディスクベースのコンテナや、直接的なアクセスが難しいオブジェクトのコンテナを実装する際の手本にもなるはずだった。だが同時に、標準コンテナの要求によってプロキシコンテナが許されないという実例にもなってしまったのだ> (p. 44)

  • <プロファイラや他のテストを実施しておけば、その最適化が本当に改善となるかどうかわかる> (p. 45) →実測が基本。

  • <vector<bool> の代わりに(略) deque<bool> を使う方が良い> (p. 46)

  • もう一回ノートしておこう。<実測上の証拠を握るまで> (p. 47) 最適化はしない。

vectordeque

  • 配列でなく vector を利用する理由は <コンテナの抽象化やカプセル化という点において配列よりも優れており、簡単かつ安全に利用できるから> (p. 48)

  • <コンテナ自体とサイズを別々に管理する必要がない> (p. 49)

  • 配列と vector の交換可能性については <現在、標準は、vector の要素が配列と同じフォーマットで連続に格納されることを要求している> (p. 50) ので、安心してレガシーコードに &v[0] を渡せる。

  • <実のところ、これらの関数を持つことが vector の欠点なのである> (p. 50) という指摘は面白い。 <これらの関数> というのは capacityreserve のことだが。

  • 今となってはおなじみの、消費メモリ削減テク「一時オブジェクトと swap イディオム」を紹介している。

setmap

何に役立てるのかはわからないが、既存 map オブジェクトのキー、すなわち map::iteratorfirst メンバーを無理やり書き換える方法を議論する。

  • <キーの比較には、必ず引数の Compare 型が使われる> (p. 55)

  • <問題となるのは、コンテナに挿入済みのキーの相対順を変更するコードだ> (p. 57)

等価なコード?

  • f が関数型マクロならば <ステートメント f(a++) はどのような意味にもなり得る> (p. 62) #define f(x) (x,x,x,x,x,x,x,x,x) の例を出して説得力を補強。 C++ ではマクロの仕様は可能な限り避ける。

  • <「無効なイテレータの使用」のバグは非常に見つけにくいからだ(チェックバージョンのライブラリ実装でデバッグするのが好きならば、話は別だが)> (p. 65) チェックバージョンは動作が当然遅いので、プロジェクト内容によってはオプションをオフにしている場合すらある。

テンプレートの特殊化とオーバーロード

個人的にテンプレートの特殊化は興味がないので読み飛ばす。それにしても、本項目オチの一文が強烈な皮肉だ。

マスターマインドゲーム

  • <厳格かつ健全なソフトウェアエンジニアリングが本書の目的である> (p. 72) が、ここで羽目を外す。<ステートメントの区切り子としてセミコロンの代わりにカンマを可能な限り使うことにした> (p. 73) やら <if/else の代わりに 3 項演算子 (?:) を使うことにした> (p. 73) などやりたい放題。

  • <inner_product() という名前がまだ気になるならば、この標準アルゴリズムを accumulate()transform() の合成だと考えればよい> (p. 81)

  • <実社会の商用コードではめったに使わないカンマ演算子> (p. 84)

最適化と効率

inline

個人的には inline は最近本当に使わなくなった。コンパイラーに任せた方が利口だもの。

  • <そもそも何を最適化したいのか定義せずに答えようとしても意味がない> (p. 88) と切り出し、候補対象を列挙していく。個人的には開発速度とビルド時間を優先したいところだ。

  • ここでも実測主義が全面に出る。この姿勢は本書中で一貫していて気持ちがいい。<通常、実測に基づいた証拠だけが真のホットスポットを言い当てることができる> (p. 90)

  • <このような場合でも、やはりプロファイラを使い、そのアドバイスにしたがって最適化を行うべきである> (p. 90)

  • 最後に <インラインコードはモジュール間の結合を強める> (p. 90) と締めくくり、読者にあてずっぽな inline の使用を戒めることを忘れない。

遅延最適化 Part 1

  • <一般に、最高のパフォーマンスを示す方法は指数拡張である> (p. 94)

  • <Koenig は、指数拡張における最良の指数が一般に 2 ではない理由を説明している> (p. 94)

遅延最適化 Part 2

  • <コピー処理を後回しにするため、String オブジェクト内部でバッファを共有させよう> (p. 95) String クラスを StringStringBuf に分離して、後者をコピー不可能なクラスとして、 String オブジェクト同士で共有する作戦らしい。

  • 「参照回数」は StringBuf の内部にあるが、これを更新するのは String のメンバー。

  • 遅延コピーを実装するので、コピーコンストラクターは「浅いコピー」になる。

  • <AboutToModify() は、まだ実行していなければ「深い」コピーを遅延実行して、内部バッファの非共有を保証する> (p. 97)

遅延最適化 Part 3

  • <operator[]() はそれほど簡単ではない> (p. 100) <少なくとも、内部表現が共有されていないことを保証する必要がある> (p. 101) どんどん話が複雑になってきて、 <シングルスレッド環境ならば、まあ、こんなところだろう> (p. 105) と不吉な伏線を張る。

遅延最適化 Part 4

  • AboutToModify の最初の if 文の条件式がスレッドセーフではないことに気付けないとダメか。

  • 脚注にいいことが書いてある。 Win32 では効率の点から <できる限りクリティカルセクションを利用すべき> (p. 111) だそうだ。

  • <「深いコピー」の間、ずっとロックを取得していることに注意しよう> (p. 113) 例示のためにちょっと手抜きをしているようだ。

  • <ロックが必要な操作は refs へのアクセスだけである、ということに注意しよう> (p. 114)

例外安全問題とテクニック

<C++ 標準ライブラリを使うのであれば、例外に備える必要がある> (p. 119)

コンストラクタの失敗 Part 1

  • オブジェクトの生存期間の開始時点は、コンストラクタが(正常に) 終了した瞬間 と考える。また、生存期間の終了時点は、デストラクタが 開始した 瞬間と考える。

  • コンストラクタが例外を投げて終了する場合、オブジェクトが存在した事実がないと考える。 <コンストラクタが成功しなかったときにデストラクタが呼ばれない> (p. 123) のだ。

コンストラクタの失敗 Part 2

  • <要するに、コンストラクタやデストラクタの関数 try ブロックのハンドラは、何らかの例外を投げて終了しなければならない、ということだ> (p. 124)

  • <これまで私は、例外に対して愛情と憎しみの環境を繰り返し抱いてきた> (p. 125)

  • 関数 try ブロックのハンドラに一旦入ると、コンストラクタ内のローカル変数はスコープから外れ、(非 static な)メンバオブジェクトは既に存在しない。

  • 標準 C++ の 15.3 paragraph 10 を読んでおくこと。

  • 最大の結論は <他の関数 try ブロックにはどれも実用的な使い道がない> (p. 128) だろう。

  • 非管理リソースの獲得は <決して初期化リストで行ってはならない> (p. 128) RAII ルールの対偶とでも言うべきか。

  • <コンストラクタの例外は伝播させなければならない> (p. 129)

捕捉されない例外

uncaught_exception() の議論だが、ここは読みとばす。

  • <標準関数 uncaught_exception() は、「現在アクティブな例外が存在するか」を知る手段である> (p. 132)

管理されていないポインタに関する問題 Part 1

  • f(g(expr1), h(expr2)) のような処理シーケンスがあるとする。直感的に g, h, expr1, expr2 のいずれかがリソース確保系統の処理であってはまずいと考えられる。

  • new でオブジェクトを生成しようとして例外によって失敗したときに、確保済みのメモリは解放されるらしい。

管理されていないポインタに関する問題 Part 2

  • <明示的なリソース確保は、独立した式で行うこと> (p. 145)

例外安全なクラス設計 Part 1

「Abrahams の保証」なる名前がついているようだ。

基本保証

例外が投げられたとしても、リソースリークは起こらない。

強い保証

例外が投げられたとしても、プログラムの状態は変更されない(コミットとロールバック)。

nothrow 保証

例外を投げない。

  • PIMPL 版 (p. 152) スワップは一見の価値あり。

  • プログラム全体の状態不変性を保証するのは明らかに厳しい。「ローカルな強い保証」という考え方で折り合いをつけるのが現実的。

例外安全なクラス設計 Part 2

クラスとクラスの間の関係について、関係を分類してそれらの性質を検討していく。

  • <「何らかの形で~を使う」という記述には、大きな自由度が残されている。TU のアダプタ、プロキシ、ラッパー、あるいは T 自身の機能を実装するため、たまたま U を使っているだけの場合まで、広範囲に適用されるからだ> (p. 157)

  • <経験を積んだ開発者でさえ、継承を使いすぎる傾向がある> (p. 157) とにかく結合を弱めることに努めること。

  • 脚注も見落とせない。<クラス X と最強の関係を持つのは friend である> (p. 158)

  • まとめ (p. 160) は 10 回くらい暗唱するといい。

継承とポリモルフィズム

多重継承を使う理由とは何か?

個人的に多重継承とは理解するのがすごく面倒なものだという印象を持っているが、本項目を読んでも、やはり敬遠したいシステムだという気持ちが拭えない。

  • ABC: Abstract Base Class - メンバ変数を持たず、純粋仮想関数だけから構成された基本クラスのこと。複数の ABC から多重継承するぶんには構わない。

    <面白いことに、継承の機構を持たない言語やモデルが、この種の多重継承をサポートしている> (p. 164)

多重継承と「シャム双生児」関数

別の基本クラスに同名同シグニチャの仮想関数が存在する場合を議論する。現実的にそういう場面に出くわすことなどないだろうと思って読んでいたら、キッチリ牽制された。

<マイクロソフト社の John Kdllin 氏によれば、 COM インターフェースの IOleObjectIConnectionPoint から派生させたクラスの作成には> それぞれの Unadvise 純粋仮想関数をオーバーライドしなければならない。

解決方法は間接的に両クラスを継承するようにすること。

純粋仮想関数と非純粋仮想関数

  • 基本クラスのデストラクタは virtual かつ public か、non-virtual かつ protected のどちらかとする。

    いずれ後者の理由を調べておく。

ポリモルフィズムの制御

  • <ポリモルフィックに使用させるクラスを限定したい場合> (p. 179) の現実的な例が欲しい。

  • private 継承と friend を組み合わせるという回答。一見、必要以上に大きなアクセス権を f1 に与えているように見えるが、Derived 固有の protected/private なメンバーがないと仮定すると、実は何ら問題ないようだ。

メモリとリソースの管理

<あなたにはできる。あなたにはできる…> (p. 184)

auto_ptr の使用

  • new, new[], delete, delete[] を峻別する。混ぜて使わない。

  • <p2 に関しては、全ての終了パスに明示的な後始末のコードを書かなければならない。たとえば、「他の処理」とコメントした部分に return; というコードで終わる条件分岐がいくつかあった場合を考えてみよう> (p. 189)

  • <T のコピーコンストラクタとコピー代入演算子が利用できない場合、T オブジェクトを要素に持つ標準コンテナはインスタンス化できない> (p. 190)

クラスメンバとしてのスマートポインタ Part 1

class X1
{
    // ...
private:
    Y* y_;
};

class X2
{
    // ...
private:
    auto_ptr<Y> y_;
};
  • <一般に、生のポインタを管理クラスでラッピングして、後始末を単純化する方法がよく使われる> (p. 193) それでも X2 の例では注意点があって、<自動生成のコピーコンストラクタとコピー代入演算子が間違ったことをする、という問題点の解決には大して役立たない。単に別の間違ったことをする> (p. 193)

  • <Y の定義を提供したくなければ、たとえ空であっても、X2 のデストラクタを明示的に実装しなければならない> (p. 194) という落とし穴もある。

  • <クラスのコピー自体に意味がなければ、それらを無効にしておかなければならない> (p. 195)

クラスメンバとしてのスマートポインタ Part 2

この項目でがんばって実装している ValuePtr のようなクラスを自作しようとは思わないが、議論の後半でクラスに traits を導入するという展開は面白い。テンプレートの特殊化で、コピーのやり方を増やせる。

フリー関数とマクロ

再帰宣言

自分自身のポインタを返す関数とやらを考える。状態マシンの実装をそれで行いたいようだ。わからん。

ネスト関数のシミュレート

C++ にはネストクラスやローカルクラスはあるのに、ネスト関数がない。

  • <優れた設計とは、モジュール間の結合を弱め、モジュール自体の凝集度を強めるものだ> (p. 215)

  • ネスト関数のポイントとしては、

    • 外側の関数の変数にアクセスできる。

    • 外側の関数内のローカル=外側の関数のいかなる外側からも呼び出せない。

  • <ローカルクラスのオブジェクトは、外部の変数にアクセスできない> (p. 219)

  • まとめに <決して商用コードの中に持ち込んではならない> (p. 223) と書いてあるな。

プリプロセッサマクロ

  • C++ には constinline といった便利なものがあるが、それでもなお <#define と書くべき理由がまだいくつか残っている> (p. 224)

  • コンパイル時の条件分岐コードは、<言うまでもなく、プリプロセッサ利用の中でも最重要に分類される> (p. 225)

  • <通常、プラットフォーム固有のコードは、ファクトリパターンを使って処理するのが最良の方法である> (p. 226)

#define

define マクロには弱点がいっぱいある。

各種トピック

この章の各項目は、どういうわけか既視感が拭えない。

初期化

  • T::T(T(u))

  • T::T(u.operator T())

  • 変数の初期化には T t(u) と書く。

  • <標準の中でも読み応えのある 8.5 節> (p. 235)

先行宣言

  • <名前空間 std に属する実体を先行宣言しようとしてはならない> (p. 238)

typedef

  • <typedef はまた、意味も追加する> (p. 240)

  • <一般に、typedef はいわゆる「間接レベルの追加」により、コードの作成、読解、変更を容易にする> (p. 242)

名前空間 Part 2

ここに書いてあるガイドラインは、ある程度の期間、実務で C++ のコードを書いていれば、皮膚感覚で身に付いているはずのものばかり。いいガイドラインだ。

  • ヘッダーファイルには using なんとかを書いてはならない。名前衝突 <意図しない武力衝突> (p. 247) が生じる可能性を高めてどうする。

  • ソースファイルにおいても、using なんとかを #include なんとかよりも前に書いてはならない。ほぼ同じ理由による。

業務用を含む自作のヘッダーファイルには std:: が山ほど書いてあるものな。

あとがき以降のノート

  • 次回作は Exceptional C++ Style だ。

  • 付録はコアな人向け。

  • 参考文献一覧。

    • 書籍の入手は日本語翻訳版が存在するものについては、いずれも容易い。

    • 論文、寄稿モノはインターネットで読めるものとそうでないものが半々くらいの印象。

  • 訳者あとがきの <(邦訳版出版に)待ちくたびれて(略) C++ に見切りをつけて他の言語に移行した方もいらっしゃるかもしれません> に笑った。他言語に移行したが、うっかり本書を購入する暇人も確かに存在する。