C++のオブジェクト指向設計では、データ隠蔽が基本原則である。しかし、特定の外部実体に対してのみ内部状態へのアクセスを部分的に開放する必要がある場合、フレンド(Friend)宣言が利用される。フレンド関係は非対称的かつ明示的に設定される特殊なアクセス権限であり、カプセル化の範囲を制御する手段として機能する。
フレンドの形態と技術的特徴
- グローバル関数をクラスのフレンドとして登録する
- 別のクラス全体をフレンドとして認める
- 特定のクラスのメンバー関数のみをフレンドにする
- フレンド関数はクラスのメンバーではないため、暗黙の
thisポインタを保持しない。 - 対象クラスの
privateおよびprotectedメンバへ直接読み書きする権限が付与される。 - 実装はクラス外で行うが、アクセス許可は対象クラス内で
friendキーワードを使用して宣言する必要がある。 - 宣言の配置は可読性とメンテナンス性を考慮し、クラスの先頭または末尾に集約するのが標準的なプラクティスである。
グローバル関数との連携例
以下に、非メンバー関数からクラス内部の観測データを出力する実装を示す。
#include <iostream>
class TelemetryRecord {
private:
double rawMeasurement;
std::string sensorTag;
public:
TelemetryRecord(double val, const std::string& tag)
: rawMeasurement(val), sensorTag(tag) {}
// グローバル関数をフレンドとして登録
friend void outputSensorDetails(const TelemetryRecord& record);
};
// クラス外での定義
void outputSensorDetails(const TelemetryRecord& record) {
std::cout << "Tag: " << record.sensorTag
<< " | Raw Value: " << record.rawMeasurement << "\n";
}
継承階層における権限の限界
クラス間でのフレンド関係は、継承ツリーに自動的に波及しない。親クラスとフレンド関係にあるクラスが、子クラスの内部データに直接アクセスすることはできない。
#include <iostream>
class CoreModule {
private:
int moduleId;
public:
CoreModule(int id) : moduleId(id) {}
// DebugEngineクラスにアクセス権限を付与
friend class DebugEngine;
};
class DerivedModule : public CoreModule {
private:
int extraParam;
public:
DerivedModule(int id, int param) : CoreModule(id), extraParam(param) {}
};
class DebugEngine {
public:
void inspect(const CoreModule& base) {
// CoreModuleのprivateメンバにアクセス可能
std::cout << "Core ID: " << base.moduleId << "\n";
}
};
int main() {
CoreModule base(101);
DerivedModule derived(202, 99);
DebugEngine debugger;
debugger.inspect(base); // 正常動作
// 以下の呼び出しはエラーとなる
// debugger.inspect(derived);
// DerivedModuleはデバッガーをフレンドとして宣言していないため
return 0;
}
関係の非推移性(Non-transitivity)
フレンド関係は推移律を満たさない。クラスXがクラスYのフレンドであり、クラスYがクラスZのフレンドである場合でも、クラスZが自動的にクラスXの内部状態にアクセスできるわけではない。各クラスは独立してフレンド宣言を行う必要がある。
class VaultA {
private:
int secretA;
friend class VaultB; // VaultBのみがsecretAにアクセス可能
};
class VaultB {
private:
int secretB;
friend class VaultC; // VaultCのみがsecretBにアクセス可能
};
class VaultC {
public:
void attemptAccess(const VaultA& a, const VaultB& b) {
// std::cout << a.secretA; // コンパイルエラー(Aのフレンドではない)
// std::cout << b.secretB; // 正常(Bのフレンドであるため)
}
};
この設計思想により、意図しないカプセル化の崩壊を防ぎ、最小権限の原則を維持しながら、特定のコンポーネント間でのみ効率的なデータ共有を実現できる。