C++の基礎 - フレンド

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

privateメンバは、そのメンバを持つクラスからのみアクセスできるが、例外的に、特定の相手にのみprivateメンバへのアクセスを許可するフレンドがある。
フレンドは、特定の関数からのアクセスを許可するフレンド関数、および、特定のクラスからのアクセスを許可するフレンドクラスの2つが存在する。

フレンドについては賛否両論あり、使用すべきではないという意見もある。
実際に、privateメンバへアクセスできる経路を作るため、使いどころには注意が必要となる。

しかし、フレンドは相手を明確に限定しているため、特定の相手のためだけにprivateメンバをpublicメンバに変更するよりもよい方法だと言える。
フレンドを使用するかどうかを決定する前に、そのクラスのメンバにできないかどうかを考えるべきである。


フレンド関数

特定の関数に対してのみアクセスを許可するようなフレンドを、フレンド関数と呼ぶ。
通常の関数、メンバ関数(staticメンバ関数も含む)、コンストラクタ、デストラクタ、オーバーロードされた演算子等の全ての関数が該当する。
また、関数テンプレートも指定できる。

friendキーワードを使用して、フレンドにする関数を指定する。これを、フレンド宣言と呼ぶ。
フレンド宣言は、他の関数からもメンバにアクセスしていることを理解しやすくするため、クラス内の先頭に記述する方がよい。

フレンド関数から、非staticメンバにアクセスする場合、そのクラスのオブジェクトが必要となる。(どのオブジェクトのメンバにアクセスするかが不明なため)
そのため、フレンド関数に渡す実引数により、オブジェクトを指定する必要がある。

以下の例において、フレンド関数がCSampleClass&型の引数を持つ理由は、どのオブジェクトのメンバにアクセスするかを明示する必要があるからであるが、
staticメンバ関数にアクセスする場合は、これは不要である。

 class CSampleClass1
 {
    friend void func(CSampleClass1& x);
    friend void CSampleClass2::func(CSampleClass1& x);  // CSampleClass2の定義が必要となる

    // ...略
 };


また、フレンド関数にオーバーロードされた関数がある場合、フレンド関数にできる関数は、引数、戻り値、const等の全てが一致したものに限られる。

 int main()
 {
    CSampleClass1 cls1,
                  cls2;
    func(cls1);  // cls2ではなくcls1にアクセスする
 }


メンバ関数をフレンド指定する場合は、そのメンバ関数が所属するクラスの定義が見える必要がある。
また、フレンド宣言の記述は、相手先のアクセス指定の影響を受けるため、上記の例でいうと、CSampleClass2::func関数はpublicにする必要がある。

以下の例では、CSampleClass1の前にCSampleClass2が必要で、CSampleClass2の前にCSampleClass1が必要という相反する要求になるため、クラスの前方宣言を行う必要がある。

 class CSampleClass1;  // クラスの前方宣言
 
 class CSampleClass2
 {
 public:  // フレンド宣言を許可するため、publicメンバにする必要がある
    void func(CSampleClass1& x);
 };
 
 class CSampleClass1
 {
    friend void func(CSampleClass1& x);
    friend void Y::func(CSampleClass1& x);
 
    // ...略
 };


以下の例では、関数テンプレートをフレンド指定している。

 template <typename T>
 void func(T a);
 
 class CSampleClass
 {
    template <typename T>
    friend void func(T);
 };


template <typename T>の箇所が、friend指定子よりも前方にあることに注意する。
テンプレート仮引数の名前については、引数や戻り値で使用しない場合は省略できる。

フレンド関数の存在意義として最も大きい理由として、演算子のオーバーロードを幅広く実現できることである。
演算子の種類によっては、クラスの外部に記述しなければならないケースがある。
しかし、クラスの外部に記述する場合、privateメンバにアクセスできないため実装が難しくなるが、フレンド関数を活用することにより簡潔に記述できる。

上記の用途以外でのフレンド関数の使用は原則として避けて、他の設計を検討した方がよい。


フレンドクラス

friend指定子はクラスに対しても使用でき、指定されたクラスをフレンドクラスと呼ぶ。
メンバ関数をフレンド関数にする場合と異なり、privateメンバ関数もフレンドになる。

フレンドクラスは、フレンド関数よりも許可を与える範囲が広いため、必要な場合に限り、フレンドクラスを使用するように検討する方がよい。

フレンドクラスを指定する場合は、friend class <クラス名>;のようにclassキーワードを付加する。
構造体の場合は、friend struct <構造体名>;を付加する。

フレンドクラスを使用する場合、クラスや構造体であることが明示できるため、その定義が見えていなくても問題ない。

また、フレンドクラスを指定する時、classstructは省略できるため、friend <クラス名または構造体名>;と記述できる。
この場合、クラスの定義が明示されていないため、エラーになる。

 class CSampleClass1
 {
    friend class CSampleClass2;  // CSampleClass2の定義が見えていないが、classキーワードがあればOK
    friend CSampleClass2;        // CSampleClass2の定義が見えていないのでエラー (ただし、クラスの前方宣言があればOK)
 };
 
 class CSampleClass2
 {
    // ...略
 };


テンプレートクラスもフレンドクラスとして指定することができる。

また、テンプレート仮引数の名前は省略することができる。
この場合、classキーワードを省略できないことに注意する。
これは、テンプレートクラス自体はクラスではないため、テンプレートクラスの定義がfriend指定子よりも前方に記述されているからといって、
テンプレートクラスが見えていることにはならないからである。

 template <typename T>
 class CSampleClass2
 {
    // ...略
 };
 
 class CSampleClass1
 {
    template <typename>
    friend class CSampleClass2;
 };



前方宣言

フレンドクラスを使用する場合、2つのクラスが互いの定義を求めることにより、どちらの定義を前方に記述してもコンパイルエラーが起きる。
この時、クラス名のみを宣言することことにより、解決できる。

以下の例では、class <クラス名2>;のことをクラスの前方宣言と呼ぶ。
構造体の場合は、struct <構造体名>;と記述する。

 class <クラス名2>;
 
 class <クラス名1>
 {
    friend <クラス名2>;
 };


前方宣言により解決できるものは、friend指定子に指定するための名前が必要である場合、または、クラス型のポインタや参照が必要な場合に限られる。
例えば、クラス型の実体を必要とする場合には、前方宣言では解決できない。(クラスの大きさが必要な場合は、前方宣言では対応できず、定義が必要)

 // クラスの前方宣言で解決できない場合
 
 class <クラス名2>;
 
 class <クラス名1>
 {
    <クラス名2> m_cls2;  // エラー (実体が必要な場合は、<クラス名2>の定義が必要)
 };


ポインタや参照は、コンパイラにはクラスの大きさが理解できるため、定義は不要である。

 // クラスの前方宣言で解決できる場合
 
 class <クラス名2>;
 
 class <クラス名1>
 {
    <クラス名2> *m_pcls2;  // OK (ポインタや参照の場合は、<クラス名2>の定義は不要)
    <クラス名2> &m_rcls2;  // OK (ポインタや参照の場合は、<クラス名2>の定義は不要)
 };


クラスのメンバへアクセスする必要がある場合は、クラスの定義が必要である。

ただし、実体の場合でも、引数や戻り値に使用するのみであれば、前方宣言で解決できる。

 class <クラス名2>;
 
 class <クラス名1>
 {
 public:
    void Set(<クラス名2> cls2);
    <クラス名2> Get() const;
 };


以下の例において、Set関数やGet関数の宣言を行うのみの場合、クラスの大きさは不要である。 クラスの大きさが必要なのはこれらのメンバ関数を呼び出している側であるため、呼び出し側には#include <クラス名2の定義があるヘッダファイル>の記述が必要となる。

また、Set関数やGet関数をインライン関数にする場合、<クラス名2>の定義が必要になる。
つまり、他のクラスを使用するようなインライン関数を記述する場合、クラスの前方宣言では解決できない。

インクルードするヘッダファイルが増加する場合は依存するソースコードの量も増加するため、コンパイルに時間が掛かる。
そこで、クラスの前方宣言を活用することにより、インクルードするヘッダファイルの量を減少させて、コンパイルに掛かる時間を削減することができる。