C++編【言語解説】 第25章 フレンド

先頭へ戻る

この章の概要

この章の概要です。

フレンド

ここまでに学んできた、クラスのメンバに対するアクセス制御の方法として、publicキーワードによる「公開」と、privateキーワードによる「非公開」の2通りがありました。

「非公開」のメンバは、そのメンバを持つクラス自身からのみアクセスできるということでしたが、実は、例外的に、特定の相手にだけ「非公開」のメンバへのアクセスを許可する機能があります。この機能は、フレンドと呼ばれ、特定の関数からのアクセスを許可するフレンド関数と、特定のクラスからのアクセスを許可するフレンドクラスの2つがあります。

まだ登場していませんが、アクセス制御の3つ目のパターンとして「限定公開」があります。フレンド関数やフレンドクラスからは、「限定公開」のメンバにもアクセスできます。「限定公開」については、第27章で解説します。

実は、フレンドは賛否両論ある機能で、「使うべきでは無い」という主張も存在します。実際、「非公開」であるはずのメンバへアクセスできる経路を作るという行為なので、使いどころには注意が必要です。しかしながら、フレンドは相手を明確に限定しているので、特定の相手のためだけに、「非公開」のメンバを「公開」に変更してしまうよりは、ずっと良い方法だと言えます。

勿論、フレンドを使うと決定する前には、素直にそのクラスのメンバに出来ないのかを考えるべきです。

フレンド関数

特定の関数に対してだけ、アクセスを許可するようなフレンドの使い方は、フレンド関数と呼ばれます。

対象は、関数とみなせるものなら何でもよく、通常の関数、メンバ関数(静的も含む)、コンストラクタ、デストラクタ、オーバーロードされた演算子といったものがすべて該当します。また、関数テンプレートを指定することもできます。

friend指定子を使って、フレンドにする関数を指定します。この記述は、フレンド宣言と呼ばれます。

フレンド宣言は、クラス定義内のどこに書いても結果は同じになりますが、他の関数からもメンバにアクセスしていることを分かりやすくするため、クラス定義の先頭近くに書いておくのが無難です

class X {
    friend void func(X& x);
    friend void Y::func(X& x);  // Y の定義が必要

    // メンバ宣言
};

フレンドとして指定された関数から、クラスの静的でないメンバをアクセスするには、そのクラスのオブジェクトが必要です。でなければ、どのオブジェクトのメンバをアクセスしたいのかが分かりません。そのため、フレンドとして指定された関数に渡す実引数によって、オブジェクトを指定する使い方が多くなります。

上のサンプルコードで、フレンド関数が X&型の引数を持っているのは、この事情を意識したものですが、staticメンバにしかアクセスしないのなら、これは必要ありません。

int main()
{
    X x1, x2;
    func(x1);  // x2 ではなく x1 をアクセスしたい
}

なお、フレンド関数にオーバーロードが存在していたとしても、フレンドになるのは、引数や戻り値、const の有無といった指定のすべてが一致したものに限られます。

メンバ関数をフレンド指定するには、そのメンバ関数が所属しているクラスの定義が見えていなければなりません。また、フレンド宣言の記述自体は、相手先のアクセス指定の影響を受けるので、上のサンプルコードで言えば、Y::funcメンバ関数は「公開」されている必要があります。

例えば、次のようになっていれば良いということですが、実はこれでもまだ問題があります。

class Y {
public:  // フレンド宣言を許可するため、「公開」されている必要がある
    void func(X& x);  // X が見えない
};

class X {
    friend void func(X& x);
    friend void Y::func(X& x);

    // メンバ宣言
};

この場合、今度は Y::func() の仮引数に使っている X が見える位置にないので、エラーになってしまいます。X の前に Y が必要で、Y の前に X が必要という相反する要求になってしまうので、他の解決策が必要です。

この問題は、クラスの前方宣言を行うことで解決できます(後の項でも取り上げます)。

class X;  // クラスの前方宣言

class Y {
public:  // フレンド宣言を許可するため、「公開」されている必要がある
    void func(X& x);
};

class X {
    friend void func(X& x);
    friend void Y::func(X& x);

    // メンバ宣言
};

関数テンプレートをフレンド指定する場合は、次のように記述します。

template <typename T>
void func(T a);

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

「template typename ・・・」の部分が、friend指定子よりも前に来ることに注意して下さい。テンプレートパラメータの名前(ここでは T)については、仮引数や戻り値のところで使用しないのならば省略できます。

フレンド関数の存在意義として最も大きいのは、演算子オーバーロードをうまく実現することです第19章では、クラス定義内で行う演算子オーバーロードのみを取り上げましたが、演算子の種類によっては、クラスの外に出さなければならないケースがあります。しかし、クラスの外に出してしまうと、「非公開」な部分にはアクセスできませんから、実装が難しくなることがあります。この場面では、フレンド関数が活用できます。この辺りの解説は、第35章で行います。

逆に、この用途以外でのフレンド関数の利用は原則として避け、他の設計を検討した方が良いでしょう。

フレンドクラス

friend指定子は、クラスに対しても使用することが出来ます。この場合、指定されたクラスはフレンドクラスと呼ばれます。

メンバ関数をフレンド関数にする場合と異なり、「非公開」なメンバ関数もフレンドになります

フレンドクラスは、フレンド関数よりも許可を与える範囲が広く、率直に言うと「やり過ぎ」な感があります。まず、本当にフレンド機能を使わなければならないのか、フレンド関数にできないのかを考え、どうしても必要な場合に限り、フレンドクラスを使用するように検討するのが良いでしょう。

フレンドクラスの指定の際には、「friend class X;」のように、classキーワードを付けることができます(構造体の場合は、structキーワードを使う)。この方法を使う場合は、X がクラスや構造体であることが明示できているので、その定義が見えていなくても問題ありません。

また、フレンドクラスの指定の際の class や struct は省略でき、「friend X;」と書くことができます。この場合は、X の正体が明示されていないため、X の定義が見えていないとエラーになります

class X {
    friend class Y;  // Y の定義が見えていないが、classキーワードがあれば OK
    friend Y;        // Y の定義が見えていないのでエラー (前方宣言があれば OK)
};

class Y {
};

クラステンプレートを、フレンドクラスとして指定することもできます。フレンド関数の場合と同様、「template typename ・・・」の部分が、friend指定子よりも前に来ます。また、テンプレートパラメータの名前については、省略できます。

template <typename T>
class Y {
};

class X {
    template <typename>
    friend class Y;
};

この場合、classキーワードを省略できないことに注意して下さい。クラステンプレートそのものはクラスではないので、上の例で言うと、クラステンプレートY の定義が、friend指定子よりも先に書かれているからと言って、クラスY が見えていることにはなりません。

C++11 (拡張friend宣言)

C++11

C++11 より前の規格では、テンプレートパラメータや typedef名を使って、フレンド宣言を行うことはできませんでした。

class Y {
};
typedef Y Y2;

template <typename T>
class X {
    friend T;    // エラー
    friend Y2;   // エラー
};

C++11 では、こういったフレンド宣言も許可されるようになりました。


前方宣言

フレンドを使おうとすると、2つのクラスが互いの定義を求めてしまい、どちらの定義を先に持ってきても、どこかでコンパイルエラーが起きてしまう状況が生まれることがあります。このような場面では、クラスの名前だけを宣言しておくことで、解決を計ることができます。

class Y;

class X {
    friend Y;
};

この例では、最初の行「class Y;」が宣言に当たり、これをクラスの前方宣言と呼びます。Y が構造体であれば struct を使っても構いません。

前方宣言によって解決できるのは、friend指定子に指定するための名前が必要である場合や、そのクラス型のポインタや参照が必要な場合などに限られます。例えば、そのクラス型の実体を必要としている場合には、前方宣言では解決できません。

class Y;

class X {
    Y mY;  // 実体が必要な場合、Y の定義が必要
};

端的に言えば、そのクラスの大きさが分からないといけない場面では、前方宣言では対応できず、定義が必要です。ポインタや参照は、指し示す型が何であれ、コンパイラにはその大きさが分かっていますから、定義は必要ありません。また、そのクラスのメンバへアクセスする必要がある場合には、その定義が必要です

前方宣言は、余計な #include を減らすためのツールとしても有効に活用できます。一般的に、1つのヘッダファイルに1つのクラス定義を記述しますから、クラスX は x.h に、クラスY は y.h のように分かれて定義されます。

// x.h

class X {
};
// y.h
#include "x.h"  // 無駄

class Y {
    X*  mX;
};

このようなケースにおいて、y.h で使っているのは クラスX のポインタなので、X という名前がクラスであることさえ分かれば十分です。そのため、x.h を #include で取り込むのではなく、X を前方宣言するだけで済みます。

// y.h
class X;

class Y {
    X*  mX;
};

また、実体であったとしても、引数や戻り値の指定に使うだけであれば、やはり前方宣言で済みます。

// y.h
class X;

class Y {
public:
    void Set(X x);
    X Get() const;
};

この場合、Setメンバ関数や Getメンバ関数の宣言をしているだけですから、クラスX の大きさの情報が必要ありません。大きさの情報を必要としているのは、これらのメンバ関数を呼び出している側です。つまり、呼び出し側の方には #include "x.h" が必要になるでしょう。

また、Setメンバ関数や Getメンバ関数をインライン関数にして、y.h に実装を記述すると、その時点で、クラスX の定義が必要になります。つまり、他のクラスを使用するようなインライン関数を使うと、前方宣言で済まなくなるという弊害があります。

#include が多くなるほど、コンパイルの際に依存するコードの量が増えていくため、コンパイルに掛かる時間が増加します。前方宣言を活用することで、#include の量を減らせば、コンパイル時間の削減に貢献します。

C++11 (列挙型の前方宣言)

C++11 からは、列挙型も前方宣言できるようになりました

従来の列挙型でも、スコープ付きの列挙型(第7章)でも可能ですが、基盤となる型を指定する場合(第7章)は、前方宣言と実際の定義に食い違いがあってはいけません。

また、従来の列挙型では、基盤となる型の指定が無い場合には、宣言を記述することはできません。

enum E1;  // 従来の列挙型の場合は、基盤となる型が必要
enum E2 : short;
enum class E3;
enum class E4 : short;

enum E1 {A};
enum E2 : short {B};
enum class E3 {C};
enum class E4 : short {D};

なお、VisualStudio の場合、基盤となる型の指定が無くても前方宣言が行えてしまうようです。


練習問題

問題① あるヘッダファイルに定義されているクラスX を使用する際、#include でなく、前方宣言だけで対応可能なものと、そうでないものとに分類して下さい。

X x1;
X* x2;
void func(X&);
X::type t;        // type は「公開」されているtypedef名
X::E1 e;          // E1 は「公開」されている enum の列挙定数
X::Enum mEnum;    // Enum は「公開」されている enum の型名
x2->func();       // func() は X の「公開」されているメンバ関数


解答ページはこちら

参考リンク



更新履歴

'2018/7/13 サイト全体で表記を統一(「静的メンバ」-->「staticメンバ」)

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

'2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。

'2017/8/4 「C++11 (列挙型の前方宣言)」を修正。
従来の列挙型では、基盤となる型の指定が無ければ宣言できないのが正しく、VisualStudio はそうなっていない。

≪更に古い更新履歴を展開する≫



前の章へ(第24章 入れ子クラスとローカルクラス)

次の章へ(第26章 派生クラス)

C++編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS