【C++】friend関数・friendクラスとは?|カプセル化の例外を使いこなす

friend keyword thumbnail C++

「friendって、カプセル化を壊すための抜け道みたいで怖い」「friend関数とfriendクラスって何が違うの?」「本当に使う場面ってあるの?」

こんな疑問はありませんか?

今回は、C++のfriend関数・friendクラスについて、実際にコンパイル・実行した結果を交えながら解説していきます。friend無しだと本当にコンパイルが通らないのかも、実際にエラーを出して確認します。

friendとは?

friendとは、

・クラスの外にある特定の関数やクラスだけに、そのクラスのprivate/protectedメンバへのアクセスを特別に許可する仕組み

です。「他人には見せない」がクラスの基本(カプセル化)ですが、friendを使うと「この関数・このクラスにだけは特別に見せる」という例外を作れます。

【実験】friendが無いとどうなる?

まずはfriendを使わずに、外部関数からprivateメンバへアクセスしようとしてみます。

class Vector2 {
private:
    float x, y;
public:
    Vector2(float x_, float y_) : x(x_), y(y_) {}
};

// friend宣言なしで、外部関数からprivateメンバに直接アクセスしようとする
void PrintVector(const Vector2& v) {
    printf("(%f, %f)\n", v.x, v.y); // v.x, v.y はprivateなのでアクセスできないはず
}

実際にコンパイルすると、次のエラーになりました。

friend_fail.cpp(12): error C2248: 'Vector2::x': private メンバー (クラス 'Vector2' で宣言されている) にアクセスできません。
friend_fail.cpp(5): note: 'Vector2::x' の宣言を確認してください
friend_fail.cpp(3): note: 'Vector2' の宣言を確認してください
friend_fail.cpp(12): error C2248: 'Vector2::y': private メンバー (クラス 'Vector2' で宣言されている) にアクセスできません。
friend_fail.cpp(5): note: 'Vector2::y' の宣言を確認してください
friend_fail.cpp(3): note: 'Vector2' の宣言を確認してください

想定通り、外部関数からはprivateメンバに触れません。ここにfriend宣言を1行足すだけで、この関数だけ特別にアクセスできるようになります。

friend関数を実装してみる

class Vector2 {
private:
    float x, y;
public:
    Vector2(float x_, float y_) : x(x_), y(y_) {}

    // friend宣言: PrintVectorだけは特別にprivateへアクセスできる
    friend void PrintVector(const Vector2& v);
};

void PrintVector(const Vector2& v) {
    printf("(%.1f, %.1f)\n", v.x, v.y); // friendなのでアクセスOK
}

クラス定義の中にfriend void PrintVector(const Vector2& v);と書くだけです。この宣言の位置(public/private)は関係なく、どこに書いても効果は同じです。

friendクラスを実装してみる

特定の関数だけでなく、クラス全体をfriendにすることもできます。デバッグ専用クラスから、本来隠したいゲームクラスの内部状態を覗きたいときなどに便利です。

class Enemy; // 前方宣言

class EnemyDebugger {
public:
    void DumpHp(const Enemy& e); // 定義はEnemyの後ろで行う
};

class Enemy {
private:
    int hp = 100;
    friend class EnemyDebugger; // EnemyDebuggerクラス全体をfriendにする
public:
    void TakeDamage(int dmg) { hp -= dmg; }
};

void EnemyDebugger::DumpHp(const Enemy& e) {
    printf("Enemy.hp = %d (friendクラス経由でprivateにアクセス)\n", e.hp);
}

friend class EnemyDebugger;と書くだけで、EnemyDebuggerすべてのメンバ関数からEnemyのprivateメンバにアクセスできるようになります。個別の関数ごとにfriend宣言を書く手間が省ける代わりに、アクセスを許可する範囲は広くなります。

実際に両方を実行した結果です。

[1] friend関数
    PrintVector(v) = (1.0, 2.0)
[2] friendクラス
    Enemy.hp = 70 (friendクラス経由でprivateにアクセス)

ダメージ30を受けてhpが100→70になった様子が、EnemyDebuggerから直接読み取れています。

friendのよくある失敗例と対処法

①friendを付けずにアクセスしようとしてエラーになる

上で実演した通りです。error C2248系のエラーが出たら、まずアクセス指定子(public/private/protected)を疑い、それでも外部からどうしても触る必要があるなら friend を検討します。

②何でもfriendにして設計が崩れる

friendは便利ですが、乱用するとカプセル化の意味がなくなります。「本当にこの関数・このクラスだけに許可すべきか」を都度考え、可能ならgetter/setter(公開メンバ関数)で済ませられないか先に検討しましょう。

③friend関係は継承されない・相互にもならない

friend class EnemyDebugger;と書いても、EnemyDebuggerを継承したクラスは自動的にfriendにはなりません。同様に、EnemyEnemyDebuggerのfriendになったからといって、EnemyDebuggerEnemyのfriendになるわけでもありません。片方向のみの許可です。

注意点

  • friendはアクセスを許可する側のクラスに書く
  • friend関数は1つの関数だけ、friendクラスはそのクラスの全メンバ関数が対象
  • friend関係は継承されず、相互でもない
  • 乱用するとカプセル化が崩れるので、本当に必要な範囲だけに絞る

まとめ

  • friendは特定の関数・クラスにだけprivateアクセスを許可する仕組み
  • friend無しでは本当にコンパイルエラーになることを実際に確認した
  • friend関数は個別に、friendクラスはまとめて許可できる
  • 継承もされず相互にもならないので、許可する範囲を意識して使う

friendは「カプセル化を破る危険な機能」と紹介されがちですが、実際にはあくまで「明示的に許可した相手だけに開ける」仕組みです。使いどころを絞れば、デバッグ用クラスや演算子オーバーロードなどで素直に役立ちます。

ここまで読んでくださりありがとうございました。

関連記事