下面列出幾個 高級友元應用場景 與典型設計模式,并配以示例,幫助大家在實際項目中靈活運用 friend
機制。
1. ADL 友元注入(“注入式友元”)
場景:為某個類型定義非成員操作符(如算術、流插入等),希望通過 Argument-Dependent Lookup(ADL)讓調用者只需 #include
類型的頭文件即可生效。
做法:在類內部直接定義 friend
函數,而不在命名空間外重復聲明。
namespace math {// 將 operator* 注入到 math 命名空間,ADL 可自動發現
class Vec {double x,y;
public:Vec(double _x, double _y): x(_x), y(_y) {}// 注入式友元friend Vec operator*(double s, const Vec& v) {return Vec(s * v.x, s * v.y);}friend Vec operator*(const Vec& v, double s) {return Vec(s * v.x, s * v.y);}friend std::ostream& operator<<(std::ostream& os, const Vec& v) {return os << '(' << v.x << ',' << v.y << ')';}
};} // namespace math// 使用方只需 #include "vec.hpp"
int main() {using namespace math;Vec v(1,2);std::cout << 3 * v + v * 4 << "\n"; // ADL 自動找到 operator*
}
2. Pimpl(編譯期隱藏實現細節)
場景:使用 Pimpl(Pointer to Implementation)模式,將實現細節完全隱藏于 .cpp
,降低編譯依賴。
做法:讓 Impl
類成為接口類的友元,直接操作私有指針與成員。
// widget.hpp
class Widget {
public:Widget();~Widget();void draw();
private:struct Impl;Impl* pImpl;friend struct Impl; // Impl 可直接訪問 pImpl
};// widget.cpp
struct Widget::Impl {int w,h;void drawImpl() { /* 繪制邏輯 */ }
};Widget::Widget(): pImpl(new Impl{640,480}) {}
Widget::~Widget(){ delete pImpl; }
void Widget::draw() { pImpl->drawImpl(); }
3. CRTP 與靜態多態“掛鉤點”
場景:在基類中提供默認行為,同時允許派生類定制實現(類似“模板回調”)。
做法:通過 friend
授權基類模板訪問派生類私有成員。
template<typename Derived>
class Serializer {
public:std::string serialize() const {// 訪問 Derived::toString()return static_cast<const Derived*>(this)->toString();}
};class Person : public Serializer<Person> {std::string name;int age;// 授權 Serializer<Person> 訪問私有成員friend class Serializer<Person>;std::string toString() const {return name + ":" + std::to_string(age);}
public:Person(std::string n,int a): name(std::move(n)), age(a) {}
};
4. 單元測試訪問私有成員
場景:在不破壞封裝的前提下,讓測試框架訪問類私有、受保護成員。
做法:在被測類內聲明測試類/測試函數為友元。
class MyClass {
private:int secret();friend class MyClassTest; // GoogleTest 測試套件
};int MyClass::secret() { return 42; }// MyClass_test.cpp
#include "MyClass.hpp"
#include <gtest/gtest.h>
class MyClassTest : public ::testing::Test { /* … */ };TEST_F(MyClassTest, Secret) {MyClass obj;EXPECT_EQ(obj.secret(), 42); // 直接訪問私有函數
}
5. 表達式模板與延遲求值
場景:在數值計算庫(如 Eigen、Blaze)中,構建 AST 節點并延遲計算,避免中間拷貝。
做法:各運算節點之間用 friend
提供訪問內部節點接口。
template<typename L, typename R>
struct AddExpr {const L& l; const R& r;AddExpr(const L& a,const R& b):l(a),r(b){}double eval(size_t i) const { return l.eval(i) + r.eval(i); }
};class Vec {std::vector<double> data;
public:double eval(size_t i) const { return data[i]; }template<typename R>friend AddExpr<Vec, R> operator+(const Vec& a, const R& b) {return {a,b};}// … 其他運算
};
6. SFINAE/Tag-Dispatch 友元函數
場景:根據類型特征啟用/禁用不同版本的非成員函數。
做法:在類內部聲明模板友元,并結合 std::enable_if
。
#include <type_traits>
class Container {// 只有當 T 為可迭代類型時,才注入該友元template<typename T,typename = std::enable_if_t<std::is_same<decltype(std::declval<T>().begin()), typename T::iterator>::value>>friend void process(const T& c) {for (auto& x : c) { /* … */ }}
};
7. C++20 模塊與友元
場景:在模塊(module; export module M;
)內部,想讓某些實現隱藏于模塊界面之外,卻又可被同模塊其它單元訪問。
做法:使用 friend
將模塊中的“私有接口”類/函數授權給模塊外可見的類型。
// M.ixx (模塊接口)
export module M;
export class PublicAPI {void foo();
};// M.cppm (模塊實現)
module M;
class Helper { /* … */ };
friend class Helper; // 僅在同模塊實現單元可訪問
void PublicAPI::foo() { /* Helper 可訪問 PublicAPI 私有 */ }
8. 好友網絡與訪問輪廓
在復雜系統中,不同子系統之間有時需要部分越權訪問,此時可定義“訪問輪廓”接口(Access
)類,將細粒度權限集中管理:
// access.hpp
class SubsysA;
class SubsysB;
class Access {
private:friend class SubsysA; friend class SubsysB;static void grant(A& a, B& b) { /* … */ }
};// subsys_a.hpp
#include "access.hpp"
class SubsysA {void doA(A& a, B& b) {Access::grant(a,b);}
};// subsys_b.hpp 同理
9. 注意事項回顧
- 最小授權:只授權必要的函數/類,避免“一放就放大”;
- 文檔化:在頭文件注釋中說明友元緣由,提醒維護者;
- 版本演進:內部私有成員改動時,及時修正友元函數簽名;
- 可替代方案:能用公有接口或策略模式解決時,優先考慮更松耦合的方式。
通過以上高級用例,可以在 域內注入操作符、隱藏實現細節、構建高性能表達式模板、精細化測試訪問、模塊內私有互操作 等多種場景下,靈活運用 friend
關鍵字,既保留封裝帶來的優勢,又實現必要的跨域訪問。