下面列出几个 高级友元应用场景 与典型设计模式,并配以示例,帮助大家在实际项目中灵活运用 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
关键字,既保留封装带来的优势,又实现必要的跨域访问。