C++中友元(friend)高级应用和使用示例

下面列出几个 高级友元应用场景 与典型设计模式,并配以示例,帮助大家在实际项目中灵活运用 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 访问私有成员
    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 
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 
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. 注意事项回顾

  1. 最小授权:只授权必要的函数/类,避免“一放就放大”;
  2. 文档化:在头文件注释中说明友元缘由,提醒维护者;
  3. 版本演进:内部私有成员改动时,及时修正友元函数签名;
  4. 可替代方案:能用公有接口或策略模式解决时,优先考虑更松耦合的方式。

通过以上高级用例,可以在 域内注入操作符隐藏实现细节构建高性能表达式模板精细化测试访问模块内私有互操作 等多种场景下,灵活运用 friend 关键字,既保留封装带来的优势,又实现必要的跨域访问。

你可能感兴趣的:(C++,c++,C++设计模式,ADL,友元,CRTP,与静态多态,C++友元函数,C++20,模块与友元,人工智能)