Modern C++ Design 読書ノート 2/3

著者:

Andrey Alexandrescu

訳者:

村上雅章

出版社:

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

発行年:

2001 年

ISBN:

978-4-89471-435-9

第 5 章 汎用のファンクタ

  • 「値のセマンティックスを伴ったオブジェクト」という言い回しが頻出する。これはオブジェクトがコピー、代入、値による引渡しが完全にサポートされていることを意味する。

  • <関数へのポインタをかなり現代風にアレンジしたもの> (p. 105)

  • <状態を保存でき、メンバ関数を起動できる> (p. 105) ので、単なる関数ポインタより有利。


Command デザインパターンの解説セクション。

  • <作業がどのようにして行われるのかを起動側は気にしなくても良い> (p. 106)

  • <呼び出しを無期限に延期できる> (p. 107)

  • <Command パターンでは、処理を実行するために必要な環境を整えるタイミングが、処理を実行するタイミングと異なっています> (p. 107)

  • <Command パターンの実装を手作業で行う場合、スケーラビリティに劣ることがあります。つまり、小さな機能を持った具体的な Command クラスを数多く記述しなければならず(それぞれが、CmdAddUser, CmdDeleteUser, CmdModifyUser といったアプリケーション中の単一動作を表します)、それぞれに特定オブジェクトに対する特定のメンバ関数を単に呼び出すだけの Execute メンバ関数を保持させなければならないのです> (p. 108): 心当たりありまくり。


  • <Command パターンにおける Command::Execute は、C++ におけるユーザ定義演算子 operator() となるべきです> (p. 111): 呼び出しシンタックスを operator() に統一することで、 Functor が他の Functor を保持できるようになるから。

  • クラステンプレート Functor を、戻り値の型が ResultType で、パラメータリストをタイプリスト TList で表現する。

  • Functor の定義は一回。Pimpl パターンにより実装を別のクラステンプレート FunctorImpl で行う。

  • FunctorImpl を部分特殊化をしまくって、引数リストのパラメータ数が十分大きくても対応できるようにしておく。

    // p. 114 より引用
    template <typename R, class TList>
    class FunctorImpl;
    
    template <typename R>
    class FunctorImpl<R, NullType>
    {
    public:
        virtual R operator()() = 0;
        virtual FunctorImpl* Clone() const = 0;
        virtual ~FunctorImpl(){}
    };
    
    template <typename R, typename P1>
    class FunctorImpl<R, TYPELIST_1(P1)>
    {
    public:
        virtual R operator()(P1) = 0;
        virtual FunctorImpl* Clone() const = 0;
        virtual ~FunctorImpl(){}
    };
    
    ...
    

Functor::operator()FunctorImpl::operator() へ転送を行う必要がある。

  • <Functor の定義内に任意のパラメータ数で全ての operator() を定義することができる> (p. 115)

    • 第 3 章で紹介した TypeAtNonStrict<...>::Result を十分な個数だけ typedef する。

      TypeAtNonStrictTypeAt のゆるゆるバージョン。

    • 力作業で operator() のオーバーロードをその個数分だけ実装する。


Functor オブジェクトの構築に関する考察。

  • FunctorImpl のサブクラス FunctorHandler を定義して、

  • Functor のコンストラクターで Pimpl メンバーデータにセットする。


「メンバ関数へのポインタ」に関する考察。

  • C++ では全てのオブジェクトには型があるが、operator.*operator->* の結果は何か違うものだ。


バインダーに関する考察。ちょっと読みにくいのと、BinderFirst しか議論していないのが惜しい。任意の位置のパラメータにバインドするバインダーの話を振らないと。


この章の残りの話題は、

  • Command パターンの話をしていたので、マクロやらアンドゥ・リドゥの話。

  • 「参照の参照」問題回避のため、traits を Functor::operator() にクッションする。

    // 例えば型 Parm1 が組み込み型でない場合、
    // p1 の型は Parm1& となる。
    // const が付いていたら const Parm1& となる。
    R operator()(
        typename TypeTraits<Parm1>::ParameterType p1,
        typename TypeTraits<Parm2>::ParameterType p2)
    {
        return (*spImpl_)(p1, p2);
    }
    
  • <典型的な 32 ビットのシステムの場合、(略)メンバ関数へのポインタは 16 バイト> (p. 132) となる。

など。

第 6 章 Singleton の実装

<Singleton デザイン・パターンの実装で「正解」というものは存在しません。(略)扱っている問題次第で最適なものとなるのです> (p. 137)


  • <static データ + static 関数 != Singleton> (p. 138)

  • <static な関数は virtual にできない> (p. 138)

  • <Singleton の実装では、2 番目のインスタンスを生成しないようにしながら、オブジェクトの生成と唯一性の管理に集中することになる> (p. 139)


  • デフォルト・コンストラクター、コピー・コンストラクター、代入演算子は private に宣言することは承知しているが、これを読むまでデストラクターも private にするのを忘れていた。


基本を説明してすぐに Singleton オブジェクトの破棄に関する議論が始まる。これ以降の議論は、デザインパターンの本ではまずお目にかかったことのないものだ。

  • <リソース・リークを避ける唯一の正しい手段とは、アプリケーションの終了時に Singleton オブジェクトを削除することです。問題は、その破棄後に、該当 Singleton に対するアクセスが発生しないようなタイミングを注意深く設定しなければならない点です> (p. 142)

  • 次のタイプの実装を Meyers の Singleton と呼ぶことにする。

    Singleton& Singleton::Instance()
    {
        static Singleton obj;
        return obj;
    }
    

    <Meyers の Singleton は、アプリケーションの終了処理における最も簡単な Singleton の破棄手段を提供しています> (p. 143)


死んだ参照の議論。Keyboard, Display, Log という 3 クラスがそれぞれ Singleton な場合で、KeyboardDisplay のエラーが Log に報告するような状況を考察する。この問題を KDL 問題と呼ぶことにする。

  • <この 3 つの Singleton を Meyers の Singleton で実装した場合、プログラムは正しく動作しないのです> (p. 144)

  • <妥当な方法は、Singleton に死んだ参照の検出をさせることです> (p. 144): Singleton::Instance で検出させることで、何らかのエラーハンドリングをする。


Phoenix Singleton なる概念を導入する。デストラクトされたオブジェクトのあったメモリに、再度オブジェクトをコンストラクトするというものだ。

  • Singleton::OnDeadReference で placement new の機能を利用し、 pInstance_Singleton オブジェクトを構築する。

  • atexit に破棄関数 KillPhoenixSingleton を登録する。 KillPhoenixSingleton では pInstance_ に対して明示的にデストラクターを呼び出す。<new を使用すると(略)コンパイラによる自動破棄が行われなくなるためです> (p. 147)

  • ちなみに atexit にはキズがある。<規格では、atexit を用いた関数の登録中に他の atexit による登録が発生した場合の定義が行われていない> (p. 147)


次の議論は、「Singleton に寿命レベルを導入する」というもの。

  • 前節の戦略だと、状態を保持するような Singleton では復活し切れないことは明白。

  • KDL 問題は「K, D よりも L のほうが長生きである」ことが表現できれば問題解決だ。

<ここで出てくる寿命の制御というコンセプトは、Singleton のコンセプトとは独立したものです。オブジェクトの寿命が長いほど、破棄が後にまわされるのです> (p. 149)

  • SetLongevity 関数の「仕様」は p. 151 のリスト参照。atexit の呼び出しを含むのがポイント。


Warning

寿命を指定する Singleton の実装法について数ページにわたる説明があるが、読むのが面倒になったのでスキップ。


マルチスレッド対応。

<共有されるグローバル・リソースというものは全て、競合条件とスレッドに関連する問題の元凶となり得るのです> (p. 155)

  • 今では有名になった手法だが、Doug Schdmit と Tim Harrison が発案した (1996) Double-Checked Locking パターンを紹介している。

    // p. 157 より引用
    Singleton& Singleton::Instance()
    {
        if(!pInstance_)
        {
            Guard myGuard(lock_);
            if(!pInstance_)
            {
                pInstance_ = new Singleton;
            }
        }
        return *pInstance_;
    }
    
  • ただし、ある種のマルチプロセッサではこのパターンが使えない。使えるか否かを決定するには <コンパイラのドキュメントを熟読しなければならない> (p. 157)

  • <少なくとも、pInstance_ の次に volatile 修飾子を置くことです> (p. 157)


これまでの分析を総合する。

  • SingletonHolder を 3 つのポリシーに分解する。

    • Creator: pInstance_ の初期化ポリシー。

    • Lifetime: 「通常」「復活アリ」「寿命制御」「無限」の 4 パターンを提供している。

    • Threading: シングルスレッド or マルチスレッド。

    // p. 160 より引用
    template <
       class T,
       template <class> class CreationPolicy = CreateUsingNew,
       template <class> class LifetimePolicy = DefaultLifetime,
       template <class> class ThreadingModel = SingleThreaded
    >
    class SingletonHolder;
    
  • <インスタンスの型は T* ではなく ThreadingModel<T>::VolatileType* です> (p. 160): マルチスレッド環境では仇になる、コンパイラによるある種の最適化処理を抑止するため。

  • <SingletonHolderDestroySingleton を呼び出すことはありません> (p. 161)

  • KDL 問題の解として、仮コードを p. 164 に掲載している。

第 7 章 スマート・ポインタ

<スマート・ポインタとは、operator-> と単項演算子 operator* を実装することによって単純なポインタとして使えるようにした C++ のオブジェクトです。スマート・ポインタは、れっきとしたポインタのシンタックスとセマンティックスに加えて、メモリ管理やロックといった処理を内部で実行することによって、指しているオブジェクトの寿命を注意深く管理するという面倒な作業からアプリケーションを解放します> (p. 167) と、スマートポインタの定義を簡潔に与えている。


  • <スマート・ポインタとは、シンタックスとある種のセマンティックスに関して、通常のポインタを模倣するような C++ のクラスです> (p. 167)

  • <既存の高品質なスマート・ポインタでは、たいていの場合、以下のコードのようにポインタの型によってテンプレート化されています> (p. 168)

    template <class T>
    class SmartPtr
    {
        ...
    
    private:
        T* pointee_;
    };
    

  • <値のセマンティックスが存在するオブジェクトとは、コピーや代入が可能なオブジェクトのことです> (p. 169)

  • ほとんどのスマートポインタには <所有権管理機能> (p. 169) が提供されている。


  • <pointee_ の型は必ず T* なのでしょうか> (p. 170)

  • operator-> のメカニズムを応用した <事前および事後の関数呼び出し> (p. 170)

  • ハンドルとポインタの類似性。 <セマンティックスと管理方法を見た場合、ハンドルはポインタと良く似たものなのです> (p. 171)


  • <メンバ関数というものはスマート・ポインタに適したものではないのです> (p. 172)

    なぜかというと、T の解放のためのメンバ関数呼び出しと、SmartPtr<T> のそれが似ていて紛らわしいから。

    SmartPtr<Printer> spRes = ...
    ...
    spRes->Release();
    spRes.Release();
    
  • <スマート・ポインタは、メンバ関数を使ってはいけないのです。つまり、 SmartPtr は非メンバ関数のみを使用するわけです> (p. 172)


所有権に関する考察。

  • ディープ・コピー方式

    • ポリモフィズムをサポートするため、コピーには T のコピーコンストラクタは利用できない。別途コピー用のポリシーを設けて、SmartPtr のテンプレート引数とする。

  • COW 方式

    • COW 方式とは、「指しているオブジェクトが最初に更新されるタイミングでコピーする」というもの。

    • スマート・ポインタではその「タイミング」が把握できそうにないので、不採用とする。

  • 参照カウント方式

    • <同じオブジェクトを指しているスマート・ポインタの総数を追跡する> (p. 176) 方式。

    • その総数カウンターを被参照オブジェクトにくっつける方式を侵入型参照カウント方式 (p. 177) と呼ぶ。

  • 参照リンク方式

    • 同じオブジェクトを指す SmartPtr が双方向リンクリスト構造をなす。リストが空になるタイミングが、被参照オブジェクトの破棄タイミングとなる。

  • 破壊型コピー方式

    • SmartPtr のコピーを行った場合、コピー元のオブジェクトが破壊される方式。 std::auto_ptr はまさにこれ。

    • <破壊型コピー方式を用いたスマート・ポインタは、値のセマンティックスをサポートしていないため、コンテナに格納することができず、たいていの場合、生のポインタを扱うのと同じくらい細心の注意を払って扱わなければならないのです> (p. 180)


<単項演算子 opearator& のオーバーロードはお勧めできない> (p. 181)


生のポインタ型への暗黙の変換はサポートしない。かわりに明示的な変換のための関数を提供する。


  • SmartPtr<T>T* の等価性テストのため、考え得る全ての組み合わせの operator== および operator!= を提供する。

  • なおかつ、SmartPtr<T>U* のテストのために、テンプレート版 (p. 187) も追加する。

  • 算術型への変換は operator bool と同じ理由で推奨できない (p. 188)


Warning

順序比較のセクションは、読むのが面倒になったのでスキップ。


<スマート・ポインタにおけるチェックの問題は、初期化時と参照外し時という 2 つのカテゴリに分類できます> (p. 192)

  • <経験則としては、ポインタのチェックを厳格に行うことから始めて、プロファイラ結果に応じて、チェックの除去が可能なスマート・ポインタを選ぶというのが良いでしょう> (p. 193)

  • <エラーを報告するための最も優れた方法は、例外をスローすることです> (p. 193)


マルチスレッド問題は付録 A まで取っておくか。

  • ここ (p. 196) で紹介されている LockingProxy での operator-> トリックはおさえること。

  • マルチスレッド参照カウント方式と、マルチスレッド参照リンク方式。

  • クラスレベルのロックと、オブジェクトレベルのロック。


これまでの分析を総合する。

  • <私たちは問題をポリシーと呼ぶ小さなクラスへと分割するのです。そして、各ポリシー・クラスでは、たった 1 つの問題を取り扱います> (p. 200)

  • <SmartPtr の宣言中に現れるポリシーの順序は、最も良くカスタマイズされるものが先頭に来るようになっています> (p. 201)

    template
    <
       typename T,
       template <class> class OwnershipPolicy = RefCounted,
       class ConversionPolicy = DisallowConversion,
       template <class> class CheckingPolicy = AssertCheck,
       template <class> class StoragePolicy = DefaultSPStorage
    >
    class SmartPtr;
    

第 8 章 オブジェクト・ファクトリ

この章のテーマは Factory Method デザインパターン。個人的には C++ では最もコードが書きにくいパターンだと思っているので、楽しく読めた。

  • <ここで問題になるのは、実際に導出を行う Derived という型名を new 演算子の起動時に記述しなければならない点です。(略)ある意味では、使用してはいけないとされているコード中の数値定数と良く似ています> (p. 209)

  • <型は必ずコンパイル時点で既知のものでなければならないのです> (p. 209)

  • <C++ におけるオブジェクトの生成では、呼び出し側と導出された具体的なクラスを束縛することになるのです> (p. 210)


  • 例えば、ライブラリーがユーザー定義クラスのオブジェクト生成を行う必要がある場合、これはオブジェクト・ファクトリが必要とされるケースのひとつだ。

  • 説明コードの DocumentManager::NewDocument では new 演算子ではなく、 CreateDocument 仮想メソッド呼び出しで(ユーザー定義型の)オブジェクトを生成する。

    • この CreateDocument のような役割を持つメソッドを Factory Method と呼ぶ。

  • 保存ファイルからオブジェクトを再現する際にも、オブジェクト・ファクトリが必要だ。


  • <何故言語自身にオブジェクトを生成するための柔軟な手段が備わっていないのでしょうか?> (p. 212)

  • <C++ でオブジェクト・ファクトリを作り出すのは難しい問題になる> (p. 213)


  • Drawing::Load コード (pp. 214-215) は現場でよく見かけるパターン。いい題材だ。

  • <唯一の問題は、オブジェクト指向にける最も重要な規則に反している点です> (p. 215)

    • 型の「タグ」に基づいて switch 文を書いている点。

    • Shape の派生クラスへの言及が、単独のソースファイルに集積される点。

    • 拡張することが難しい。

  • クラス ShapeFactoryShape 派生オブジェクトを生成する関数のポインタのマップを管理する。

    • Shape 派生クラスの実装ファイルで、p. 217 のコードを機械的に記述すればよい。

    • <このコードは、std::map クラス・テンプレートに馴染みのない方にとっては、若干説明が必要かもしれません> (p. 218) とあるが、そんな方はこの本の読者なんかやってないと思う。


マップのキーを整数型ではなく、もっとそれらしいものにしようではないかという議論。

<こういったことから帰結できる唯一の結論は、型識別子の管理はオブジェクト・ファクトリ自身の管轄ではないということです。 C++ という言語が、ユニークで永続的な型 ID を保証していない以上、型 ID の管理はプログラマが対処しなければならない問題なのです> (p. 219)


ここからオブジェクト・ファクトリの一般化について議論する。

  • <ファクトリは具体的な成果物を知る必要がない> (p. 220) ので、「具体的な成果物」は Factory のテンプレートパラメータにならない。

  • <エラー時の取り扱いコードを CreateObject メンバ関数から無くし、 FactoryError ポリシーに分離しなければなりません> (p. 221)


template
<
   class AbstractProduct,
   class IdentifierType,
   class ProductCreator = AbstractProduct* (*)(),
   template <typename, class>
      class FactoryErrorPolicy = DefaultFactoryError
>
class Factory;

Todo

クローン・ファクトリは読みとばす。「共変の戻り型」の用語解説はおさえておくこと。


オブジェクト・ファクトリは通常 Singleton であることが自然。

// p. 228 より引用
typedef SingletonHolder< Factory<Shape, std::string> > ShapeFactory;

以前の FunctorProductCreator とすることも可能。

// p. 228 より引用
typedef SingletonHolder
<
   Factory<Shape, std::string, Functor<Shape*> >
>
ShapeFactory;