条項31:ファイル間のコンパイル依存関係を最小限に抑える

8478 ワード

『Effective C++中国語版第三版』読書ノート
**条項31:ファイル間のコンパイル依存関係を最小化**
C++プログラムのclass実装ファイルを軽く変更したとします.修正したのはインタフェースではなく、実装であり、private成分だけを変更します.
その後、このプログラムを再構築し、数秒で済むと予想されています.「Build」を押したりmakeを入力したりすると、世界全体が再コンパイルされリンクされていることに気づきます.
問題は,C++が「実装からインタフェースを分離する」ことをうまくやっていないことである.classの定義式はclassインタフェースだけでなく、十分な実装の詳細も含まれています.
class Person{ 
public: 
    Person(const std::string& name, const Date& birthday, const Address& addr); 
    std::string name() const; 
    std::string birthDate() const; 
    std::string address() const; 
    ... 
private: 
    std::string theName;        //     
    Date theBirthDate;          //     
    Address theAddress;         //     
};

このclass Personはコンパイルできません.Person定義ファイルの一番上にはこのようなものが存在する可能性があります.
#include  
#include "date.h" 
#include "address.h"

これにより,Person定義ファイルとその含むファイルとの間にコンパイル依存関係(compilation dependency)が形成される.これらのヘッダファイルのいずれかが変更された場合、またはこれらのファイルに依存する他のヘッダファイルが変更された場合、Person classに含まれる各ファイルは再コンパイルされ、Person classを使用するファイルも再コンパイルされなければなりません.このようなシリアルコンパイル依存関係(cascading compilation dependencies)は、多くのプロジェクトに形容しがたい災難をもたらす.
なぜC++はclassの実装細目をclass定義式に置くことを堅持しているのか.なぜPersonをこのように定義しないのか、実現の詳細を分けて述べる.
namespace std { class string;} //     (   ) 
class Date;//      
class Address;//      

class Person{ 
public: 
    Person(const std::string& name, const Date& birthday, const Address& addr); 
    std::string name() const; 
    std::string birthDate() const; 
    std::string address() const; 
    ... 
};

この場合、PersonのクライアントはPersonインタフェースが変更されたときにのみ再コンパイルされます.
2つの問題:1つ目はstringはclassではなくtypedefです.そのためstringの前置き宣言は正しくなく、標準ライブラリの一部を手動で宣言しようとするべきではありません.適切なincludesだけで目的を達成する必要があります.標準ヘッダファイルがコンパイルのボトルネックになる可能性はあまりありません.
第二に、コンパイラはコンパイル中にオブジェクトのサイズを知る必要があります.
int main() 
{ 
    int x; 
    Person p(params); 
}

コンパイラはPersonを配置するのに十分な空間を割り当てる必要があることを知っていますが、Personオブジェクトがどれだけ大きいかを知っていなければなりません.この情報を得る唯一の方法はclass定義式を尋ねることです.しかし、class定義式が実装の詳細を合法的にリストしない場合、コンパイラはどのようにして割り当てられる空間を知っていますか?
この問題はsmalltalk,javaなどの言語では存在しません.なぜなら、コンパイラはオブジェクトをその言語で定義するときにポインタ(オブジェクトを指す)に十分なスペースしか割り当てられないからです.つまり、これらのコードはこのように見なされます.
int main() 
{ 
    int x; 
    Person* p; 
}

もちろんこれも合法的なC++コードなので、「ポインタの後ろにオブジェクトを実装する」ゲームをすることができます.Personを2つのclassesに分割することができ,1つはインタフェースを提供し,もう1つはインタフェースを実現する責任を負う.実装を担当するいわゆるimplementation classはPersonImplと名付けられ、Personは以下のように定義される.
#include  
#include  
class PersonImpl; 
class Date; 
class Address; 

class Person{ 
public: 
    Person(const std::string& name, const Date& birthday, const Address& addr); 
    std::string name()const; 
    std::string birthDate() const; 
    std::string address()const; 
    ... 

private: 
    std::tr1::shared_ptr pImpl; //          
};

ここで,Personは1つのポインタメンバーのみを含み,その実装クラス(PersonImpl)を指す.このデザインはpimpl idiom(pimplは「pointer to implementation」の略)と呼ばれることが多い.
これにより、Personのお客様はDate、Address、Personの実現の細部から完全に分離されます.それらのclassesの実装変更は、Personクライアントの再コンパイルを必要としません.
この分離の鍵は「定義された依存性」を「宣言の依存性」に置き換えることにある.それはコンパイル依存性の最小化の本質である.ヘッダファイルをできるだけ自己満足させ、万が一できない場合は、他のファイル内の宣言式(定義式ではなく)に依存させる.他のすべてのことはこの簡単な戦略に由来しています.
object referenceまたはobject pointerでタスクを完了できる場合は、objectsを使用しないでください.
このタイプを指すpointerとreferenceは、宣言式のみで定義できます.しかし、あるタイプのobjectsを定義するには、そのタイプの定義式が必要です.
可能な場合は、class定義式をできるだけclass宣言式に置き換えます.
関数を宣言してclassを使用する場合、classの定義式は必要ありません.関数がby valueでこのタイプのパラメータ(または戻り値)を渡しても、次のようになります.
class Date; // class     
Date today(); 
void clearAppiontments(Date d);

宣言today関数とclearAppointments関数はDateを定義する必要はありませんが、誰かがそれらの関数を呼び出すと、呼び出す前にDate定義式が露出しなければなりません.「class定義式の提供」(#includeで完了)の義務を「関数宣言の場所」のヘッダファイルから「関数呼び出しを含む」クライアントファイルに移動できる場合は、「本当に必要なタイプ定義ではない」とクライアント間のコンパイル依存性を除去できます.
宣言式と定義式に異なるヘッダファイルを提供します.
したがって、ライブラリのお客様は、いくつかの関数を事前に宣言するのではなく、常にinlcudeの宣言ファイルを保持する必要があります.
#include "datefwd,h" //          class Date 
Date today(); 
void clearAppointments(Date d);

宣言式のみを含むヘッダファイルの名前は「datefwd.h」で、標準ライブラリのヘッダファイルのようです.彼は「本条項はtemplatesにもnon-templatesにも適用される」と明らかにした.多くのコンストラクション環境ではtemplate定義式が常にヘッダファイルに配置されていますが、tamplates定義式を「非ヘッダファイル」に配置できるコンストラクション環境もあります.これにより、宣言式のみを含むヘッダファイルをtemplatesに提供できます.
このようなpimpl idiomを用いたclassesは、Handle classesと呼ばれることが多い.
このclassesの方法の1つは、それらのすべての関数を対応する実装クラス(implementation classes)に渡し、後者によって実際の作業を完了することである.
#include "Person.h" 
#include "PersonImpl.h" 

Person::Person(const std::string& name, const Date& birthday, const Address& addr) 
    : pImpl(new PersonImpl(name, birthday, addr)) 
{} 

std::string Person::name() const 
{ 
    return pImpl->name(); 
}

もう一つのHandle classを作成する方法は、Personを特殊なabstract base class(抽象ベースクラス)と呼び、Interface classesと呼ぶことです.このclassの目的はderived classesのインタフェースを詳細に記述することであるため、通常はメンバー変数も構造関数もなく、1つのvirtual解析関数とpure virtual関数のセットだけであり、インタフェース全体を記述することである.
Personのために書いたInterface classはこう見えるかもしれません.
class Person{ 
public: 
    virtual ~Person(); 
    virtual std::string name() const = 0; 
    virtual std::string birthday() const = 0; 
    virtual std::string address() const = 0; 
    ... 
};

このclassのお客様は、Personのpointersとreferenceでアプリケーションを作成する必要があります.「pure virtual関数を含むPerson classesに対してエンティティを表示することはできません.Interface classのインタフェースが変更されない限り、お客様は再コンパイルする必要はありません.
Interface classのお客様は、このclassの新しいオブジェクトを作成する方法が必要です.通常、特殊な関数が呼び出されます.この関数は、「本当に具現化される」derived classのコンストラクション関数の役割を果たします.通常、ファクトリfactory関数またはvirtual構造関数と呼ばれます.これらはポインタ(またはより望ましいスマートポインタ)を返し、interface classのインタフェースをサポートする動的に割り当てられたオブジェクトを指します.このような関数はinterface class内でstaticとして宣言されることが多い.
class Person{ 
public: 
    ... 
    static std::tr1::shared_ptr 
    create(const std::string& name, const Date& birthday, const Address& addr); 
};

お客様は、次のように使用します.
std::string name; 
Date dateBirth; 
Address address; 
std::tr1::shared_ptr pp(Person::create(name, dateBirth, address)); 
... 
std::cout << pp->name() 
            << "was born on " 
            << PP->birthDate() 
            << " and now lives at " 
            << pp->address(); 
...

もちろんinterface classインタフェースをサポートするそのイメージクラス(concrete classes)は定義され、真の構造関数は呼び出されなければならない.
継承されたvirtual関数の実装を提供するderived class RealPersonがあると仮定します.
class RealPerson : public Person{ 
public: 
    RealPerson(const std::string& name, const Date& birthday, const Address& addr) 
    : theName(name), theBirthDate(birthday), theAddress(addr) 
    {} 
    virtual ~RealPerson(){} 

    std::string name() const; 
    std::string birthDate() const; 
    std::string address() const; 

private: 
    std::string theName; 
    Date theBirthDate; 
    Address theAddress; 
};

RealPersonができたら、Person::createは本当に珍しくありません.
std::tr1::shared_ptr Person::create(const std::string& name, const Date& birthday, const Address& addr) 
{ 
    return std::tr1::shared_ptr(new RealPerson(name, birthday, addr)); 
}

より現実的なPerson::createインプリメンテーションコードは、追加のパラメータ値、独自のファイルまたはデータベースのデータ、環境変数など、異なるタイプのderived classオブジェクトを作成します.
RealPersonは、インターフェースクラスの2つの最も一般的なメカニズムの1つを実装します.インターフェースクラスからインタフェース仕様を継承し、インタフェースがカバーする関数を実装します.
handle classesとinterface classesは、インタフェースと実装との結合関係を解除し、ファイル間のコンパイル依存性を低減します.
handle classedでは、メンバー関数はimplementation pointerでオブジェクトデータを取得する必要があります.それはアクセスごとに間接性を高めます.オブジェクトごとに消費されるメモリには、implementation pointerのサイズを追加する必要があります.implementation pointerは、動的割り当てを指すimplementation objectを初期化する必要があるため、動的メモリ割り当てによる追加のオーバーヘッドも発生します.
Interface classesは、各関数がvirtualであるため、関数呼び出しのたびに間接ジャンプを払う必要があります.また、Interface classから派生するオブジェクトには、vptr(virtual table pointer)が含まれている必要があります.
プログラム開発中にhandle classとinterface classを使用して、コードが変更されたときに顧客に最小限の衝撃を与えます.
一方、速度および/またはサイズの差が大きすぎてclass間の結合相形の下でキーにならない場合、handle classおよびinterface classは、イメージクラス(concrete class)に置き換えられる.
**覚えておいてください:**
  • が「コンパイル依存最小化」をサポートする一般的な構想は、宣言式に依存し、定義式に依存しないことである.この構想に基づく2つの手段はHandle classとInterface classである.
  • ライブラリヘッダファイルは、「完全かつ音声のみ」(full and declaration-only forms)として存在する必要があります.このやり方はtemplatesにかかわるかどうかにかかわらず適用される.