Modern C++ Design 読書ノート 1/3¶
- 著者:
Andrey Alexandrescu
- 訳者:
村上雅章
- 出版社:
ピアソン・エデュケーション
- 発行年:
2001 年
- ISBN:
978-4-89471-435-9
ローマ数字ページ各種¶
本書推薦その 1 は Scott Meyers が寄せている。言うまでもなく Effective C++ シリーズの著者だ。
<パターン自身をコード化するのではなく、パターンの実装を自動的に生成させる> (p. x)
推薦その 2 は John Vlissides が寄せている。GoF の一人。
<テンプレート・パラメータによって、実行時のオーバーヘッドをまったく発生させずに、実装上のトレード・オフを変えることもできるのです> (p. xiii)
著者によるまえがきで、本書の目的を明瞭に示している。
対象読者タイプ 1: <C++ プログラマ経験者で最新のライブラリ作成テクニックをマスターしたいと考えている方々>
対象読者タイプ 2: <忙しいプログラマで手っ取り早く作業を済ませてしまう必要がある方々> (p. xviii)
<Loki という実在する C++ ライブラリ> (p. xviii) を解説する本である。
本書中のコード例では、Herb Sutter 式コーディング規則を採用。もしかして Exceptional C++ Style が関係ある?
訳者まえがき
<ある種の意思決定は実装段階でなければ行えない> (p. xxiii)
<センス・オブ・ワンダーの渦巻く C++ の新世界をお楽しみ下さい>
第 1 章 ポリシーを基にしたクラス・デザイン¶
ポリシーとポリシー・クラスとは、重要なクラス設計技術のひとつらしい。
ポリシー とは、<特定の動作や構造上の側面を専門的に受け持った小さなクラス群> (p. 3) のこと。
<プログラミングの天才が若年層に分布しているのに対して、ソフトウェア設計の天才がより高年齢層に分布する傾向にある> (p. 4)
<システム・アーキテクチャには、「設計によってある種の公理を強制する」という重要な目標があります> (p. 4)
<理想的には、設計によって課された制約のほとんどが、コンパイル時点で強制されるべきなのです> (p. 4)
<組み合わせ爆発と真っ向勝負を挑んではいけない> (p. 5)
<設計指向のライブラリ> (p. 5)
<いくつかの吟味された基底クラスを用意しておき、多重継承によって設計の選択における組み合わせの爆発を取り扱うというアイディアが考えられます。(略)しかし、少しでもクラス設計の経験があれば、こういった単純な設計がうまく機能しないことは理解できるでしょう> (p. 6)
<クラス・テンプレートは、通常のクラスではサポートされていないような方法でカスタマイズすることが可能です。もし特殊なケースを実装したいのであれば、特定のクラス・テンプレートを生成する際に、そのテンプレートにおける任意のメンバ関数を特殊化することができるのです。例えば、
SmartPtr<T>
というテンプレートがある場合、Smart<Widget>
における任意のメンバ関数を特殊化することができるわけです> (p. 6)<さらに、多重パラメータのクラス・テンプレートを用いた場合、部分的にテンプレートの特殊化を行うこともできます> (p. 6)
// こういう定義がある場合、
template <class T, class U> class SmartPtr { ... };
// この定義は部分特殊化となる。
template <class U> class SmartPtr<Widget, U>{ ... };
<多重継承とテンプレートには相補的なトレード・オフが存在しています> (p. 7)
ポリシーは traits と通じるものがあるが、型ではなく動作を強調する点が異なる。
ポリシーは「コンパイル時 Strategy パターン」と見ることもできる。
<ポリシーは、シグネチャ指向ではなく、シンタックス指向なのです> (p. 8) <例えば、
Creator
ポリシーではCreate
がstatic
であるかvirtual
であるかを規定しません> (p. 8)ホスト または ホスト・クラス とは、1 つ以上のポリシーを利用するクラスのことを指す。
テンプレート・パラメータに template
を用いる技法が紹介されている。自分ではこういうクラスを定義することはないが、利用する可能性があるので覚えておくこと。
ポリシーを使用することで得られる柔軟性
実体化する際のテンプレート引数を変更するだけで、簡単に外部からポリシーを変更できる。
アプリケーションに特化した自作ポリシーを提供できる。
ポリシー・クラスのデストラクタについて言及あり。
ポリシーに対してデストラクタを
virtual
にすると、ポリシーの性質が静的ではなくなる。効率のよい解決策としては、デストラクタを非virtual
かつprotected
とする。
「不完全実体化」の技法について解説あり。
「あるクラス・テンプレートのメンバ関数が使用されない場合、それは実体化されることがない」すなわちコンパイルエラーが起こらない。
<ポリシーに基づいたクラス設計を行う上で最も難しいのは、クラスの機能を正しくポリシーへと分解することです> (p. 20)
<極端に言えば、ホスト・クラスから固有のポリシーを完全に無くしてしまうのです> (p. 20)
<過度に一般化されたホスト・クラスの欠点は、テンプレート・パラメータが氾濫するということです> (p. 20)
typedef
の使用によって、秩序だった使用と容易な保守性が保証される。<クラスをポリシーに分解する際、 直交性のある (orthogonal) 切り口を見つけ出すことが大変重要になります> (p. 21)
<お互いが完全に独立した> (p. 21) 役割になるように、ポリシー分割するのがよいということだな。あるポリシーが別のポリシーに干渉するようではまずい。
第 2 章 テクニック¶
静的チェックの必要性。すなわち、コンパイル時版
assert
だ。 <評価される式がコンパイル時の定数になるのであれば、実行時ではなく、コンパイル時にチェックを行うことができるはずです> (p. 26)
template<bool> struct CompileTimeError;
template<> struct CompileTimeError<true>{};
#define STATIC_CHECK(expr) \
(CompileTimeError<(expr) != 0>())
上記のマクロからスタートし、エラーメッセージをなるべく読み易くするように工夫を重ねていく。
省略記号
(...)
を用いた関数宣言を利用する。
template <class Window, class Controller>
class Widget
{
...
};
// テンプレート全体を明示的に特殊化する場合の例。
template <>
class Widget<ModalDialog, MyController>
{
...
};
// 任意の Window や MyController に対して特殊化する場合(部分特殊化)。
template <class Window>
class Widget<Window, MyController>
{
...
};
<クラス・テンプレートの部分的な特殊化では、テンプレート引数の一部だけを記述し、その他の引数を元のままにしておく> (p. 29)
<テンプレートの部分的な特殊化は、メンバ関数、非メンバ関数を問わず、関数には適用されません> (p. 30) が、こういう場合は関数のオーバーロードを併用する。
ローカルクラスの話題だが、他のプログラミング言語を知っている人間なら驚かないかも。
ローカルクラスの特徴
ローカルクラスでは
static
メンバを定義できない。ローカルクラスから(それを含む関数定義内にある)非
static
のローカル変数にアクセスできない。関数テンプレート内でローカルクラスを定義することができ、それを囲んでいる関数のテンプレート・パラメータを用いることもできる。
template <int v>
struct Int2Type
{
enum { value = v };
};
例えば <
Int2Type<0>
とInt2Type<1>
は異なった型> (p. 31) となる。このようなクラスの応用例として、p. 33 のNiftyContainer::DoSomething
を覚えておくこと。
template <typename T>
struct Type2Type
{
typedef T OriginalType;
};
Type2Type
もオーバーロード関数の仮引数の型としてだけ利用する。p. 35 の関数テンプレートCreate
のオーバーロードに注目。
<ブーリアン定数によって様々な型から特定の型を選択しなければならない場合> (p. 35)
template <bool flag, typename T, typename U>
struct Select
{
typedef T Result;
};
template <typename T, typename U>
struct Select<false, T, U>
{
typedef U Result;
};
template <typename T, bool isPolymorphic>
class NiftyContainer
{
// ここでコンテナの収容型を typedef するのに
// Select を利用できる。
};
<何も情報が与えられていない 2 つの型
T
とU
がある場合、U
がT
を継承しているかどうか、そのようにすれば判るのでしょうか> (p. 37)<任意の型
T
が任意の型U
への自動変換をサポートしているかどうかは、どのように検出すればよいのでしょうか> (p. 37)脚注にいいことが書いてある。<
sizeof
はいずれにしても型を検出しなければならないため、typeof
とsizeof
は明らかに同じバックエンドを共有しているのです> (p. 37)解決策は、まず p.38 のアイディアをコードに落として、それから p. 39 のクラステンプレート
Conversion
の中にすべて閉じ込めるというもの。省略記号をとる関数オーバーロード、定義なし関数宣言とsizeof
のペアをうまく組み合わせている。
<
typeid
演算子とは、type_info
オブジェクトへの参照を返すものです> (p. 40) 個人的にはこれまでの C++ 経験でtypeid
を利用した記憶がない。type_info
の特徴 (p. 41)name
というメンバ関数があるが、クラス名を文字列に対応づける方法は標準化されていない。before
メンバ関数がtype_info
型の順序関係を定義する。コピーコンストラクタと代入演算子が無効化されている。何が言いたいかというと、「値」を何か変数に格納できないということ。
typeid
が返すオブジェクトは静的記憶域内に存在する。
使いにくいので、ラッパークラスを定義する。
class NullType{};
struct EmptyType{}; // 継承を許す。
<特性 (traits) とは、値に基づく決定が実行時に行えるのと同様に、型に基づく決定をコンパイル時に行えるようにするジェネリックなプログラミング・テクニックです (Alexandrescu 2000a)> (p. 43)
std::copy
の実装にこの技法が採用されていることが多いようだ。<ある型
T
のオブジェクトを引数として関数間で授受する場合、(略)一般的に最も効率の良い方法とは、複雑な型を参照で、スカラ型は値で引き渡すことです> (p. 46)<ここで注意が必要なのは、C++ では参照への参照が許されないという点です> (p. 47)
std::bind2nd
とstd::mem_fun
を組み合わせた場合に、このエラーが発生することも言及している。<型が
enum
かどうかを判断する方法は存在しない> (p. 47)enum
と言えば、p. 49 のコードを見て知ったが、関数定義の中でenum
を定義できるようだ。
第 3 章 タイプリスト¶
この章を真面目に読めば読むほど疲れる。理解できなくて構わないから、気になるところだけ書き留めておく。
<Abstract Factory では、設計時点で確定している型毎に、1 つずつ仮想関数を定義します> (p. 53) 「設計時点で確定している型毎」というのがミソ。Abstract Factory をライブラリー化しづらいことを示唆している。
<根幹となるコンセプトを一般化することができなければ、そのコンセプトの具体的な実体も一般化することができません> (p. 54)
<テンプレート・パラメータの数を可変にすることはできない> (p. 55)
<仮想関数はテンプレートにできない> (p. 55) 言われてみればそうだった。
template <class T, class U>
struct Typelist
{
typedef T Head;
typedef U Tail;
};
<テンプレート・パラメータには、同じテンプレートの別な実体化を含む任意の型を指定できる> (p. 56) ので、
U
をガンガン入れ子にすることでTypelist
を伸ばす。
<タイプリストは Lisp 的> (p. 57) なので、色々補助的なマクロを用意する。
typedef Typelist<signed char,
Typelist<short int,
Typelist<int, Typelist<long int, NullType> > > >
SignedIntegrals;
#define TYPELIST_1(T1) Typelist<T1, NullType>
#define TYPELIST_2(T1, T2) Typelist<T1, Typelist_1(T2) >
...
typedef TYPELIST_4(signed char, short int, int, long int)
SignedIntegrals;
以下、延々と「コンパイル時に Typelist
の情報を得る機能」の実装が続く。
<C++ でコンパイル時プログラミングに用いることができる道具は、テンプレート、コンパイル時の整数計算、型定義 (
typedef
) です> (p. 59)<C++ 自体は命令型言語に限りなく近い位置づけなのですが、コンパイル時に行われる全ての計算処理は、値の変更を行うことができない関数型言語を思い出させるようなテクニックに頼らなければならないわけです> (p. 59)
<単純に線形化されたものとしてタイプリストにアクセスすることができれば、タイプリスト操作が用意になるはずです> (p. 60)
<しかし、タイプリストの場合、こういった時間はコンパイル中に発生するものであり、コンパイル時間というものはある意味「無料」なのです> (p. 61) とあるが、脚注で言い訳しているように、現場でコンパイル時間をタダとみなせるようなことはない。
<再帰を用いて古典的な線形探索を実装する> (p. 61) ことで、タイプリストから型を検索する機能を記述できる。
残りはザッと読み流してよいが、次のトピックは後で読み返すことになる。
タイプリストを部分的に並び替える。特に、型を継承階層の下層から順に並び替えたりする機能(
struct DerivedToFront
,struct MostDerived
)タイプリストを利用して、クラス階層を一気に構築する機能 (
GenScatterHierarchy
,Tuple
,GenLinearHierarchy
)
第 4 章 小規模オブジェクトの割り当て¶
以下のノートでは std::size_t
を単に size_t
と書く。
この章で言う小規模オブジェクトとは、数バイト程度のメモリーを消費するものらしい。
operator new
とoperator delete
は <汎用目的の演算子であり、小規模オブジェクトの割り当てには向いていない> (p. 83): 本章で紹介するアロケータは、それらよりも処理速度は数段優れ、メモリー消費も半分以下だと豪語している。
デフォルトのアロケータについて。
<通常の場合、デフォルトのアロケータというものが、C のヒープアロケータを薄いラッパで包み込んだ形で実装されているため> (p. 84) 恐ろしく遅い。
遅いだけでなく、<小規模オブジェクトに対するスペース効率も非常に悪い> (p. 84): 管理用のメモリを余分に食うためとのこと。
「メモリ・アロケータの作業」に書かれているメモリレイアウトの理解が面倒。パス。
本章で解説している小規模オブジェクト・アロケータは 4 層構造。下位層から上位層へ向かって
Chunk
,FixedAllocator
,SmallObjAllocator
,SmallObject
となっている。
Chunk
は「固定長ブロックを保持するメモリのチャンク」を保持・管理する。
// p. 87 より引用。細部省略。
struct Chunk
{
void Init(size_t blockSize, unsigned char blocks);
void* Allocate(size_t blockSize);
void Deallocate(void* p, size_t blockSize);
void Release();
unsinged char* pData_;
unsinged char firstAvailableBlock_;
unsinged char blocksAvailable_;
};
関数の引数にやたらサイズがあるのは、<上位層がブロック・サイズを管理するべき> (p. 88) だから。
<効率性を考慮し、
Chunk
にはコンストラクタ、デストラクタ、代入演算子を定義しません> (p. 88)255 (
UCHAR_MAX
) ブロック以上のチャンクを保持できないことに注意。<未使用ブロックの最初のバイトには、次の未使用ブロックのインデックスを保持します> (p. 88): 例えば
Chunk::Init
の実装でpData_[i * blockSize] == (i + 1) * blockSize
となるように配列の中身を埋める。Chunk::Allocate
の実装を見ると、処理時間は \(O(1)\) になっているようだ。必然的にChunk::Deallocate
も \(O(1)\) になる。
FixedAllocator
は Chunk
の vector
として実装する。
// p. 91 より引用。
class FixedAllocator
{
size_t blockSize_;
unsigned char numBlocks_;
typedef std::vector<Chunk> Chunks;
Chunks chunks_;
Chunk* allocChunk_;
Chunk* deallocChunk_;
...
};
allocChunk_
は「前回の割り当てに使用したチャンク」とする。これに余裕がまだあれば、次の割り当てでもここを使用することで効率化できる。deallocChunk_
「直前に開放されたチャンク」だが、扱いがちょっと難しい。
<SmallObjAllocator
は、いくつかの FixedAllocator
オブジェクトを集約することによって実現されています> (p. 94)
// pp. 94-95 参照。
class SmallObjAllocator
{
std::vector<FixedAllocator> pool_;
FixedAllocator* pLastAlloc_;
FixedAllocator* pLastDealloc_;
public:
SmallObjAllocator(size_t chunkSize, size_t maxObjectSize);
void* Allocate(size_t numBytes);
void Deallocate(void* p, size_t size);
...
};
Deallocate
の引数のサイズが、ここでは「解放するサイズ」を意味する。高速に解放するため。<「効率的な」やり方は、常に「効率的な」やり方とは限らない> (p. 95)
<メモリ保持のために若干探索速度を犠牲にする> (p. 95) ことにした。
pool_
をブロックサイズに従ってソートしておくと、バイナリ・サーチが適用できる。
SmallObject
はほぼ教科書通りのインターフェイスになる。
// p. 96
class SmallObject
{
static void* operator new(size_t size);
static void operator delete(void* p, size_t size);
virtual ~SmallObject();
};
デストラクタは仮想でなければならない。理由は
operator delete
に引き渡されるサイズを正しくさせるため。operator new
の実装でSmallObjAllocator::Allocate
を利用する。またoperator delete
でSmallObjAllocator::Deallocate
を利用する。ということは、
SmallObjAllocator
は Singleton でなければならない。
各種ポリシーをくっつけて SmallObject
をクラステンプレートにして仕上がる。本章ではここまでテンプレートがなかなか出てこなかった感があるが、ここでようやく登場。
// p. 100 より引用。
template
<
template <class T>
class ThreadingModel = DEFAULT_THREADING,
size_t chunkSize = DEFAULT_CHUNK_SIZE,
size_t maxSmallObjectSize = MAX_SMALL_OBJECT_SIZE
>
class SmallObject;
<保守的ということは最適ではないということを意味しているのです> (p. 101)