A.11. 友元机制

C++ 提供了一个在其他语言中不常见、且常被误解的访问限定符:friend(友元)。一个类可以将另一个类或函数声明为自己的友元,从而赋予该友元对这个类所有成员的完全访问权限,包括那些声明为 protected 或 private 的成员。

有些人认为 friend 会破坏封装性。确实,如果使用不当,它可能会带来这样的问题。但其设计初衷是为了向特定的实体授予特殊的访问权限,而不是将这些成员暴露为 public 或 protected,那样反而会造成更严重的封装破坏。

举个例子,考虑以下类:thing 是一种需要由 thing_factory 根据名为 name 的文件内容构建的对象,而 thing_factory 在构造 thing 之前能够验证文件内容的合法性:

class thing {
  thing(string_view); // 注意:私有成员
  // ... 各种有趣的成员
  // thing_factory 可以访问 thing 类的私有成员
  // class thing
  friend class thing_factory;
};

// 在读取到格式错误的文件时抛出此异常
class invalid_format{};

class thing_factory {
  // ... 这里也有一些各种有趣的实现
  string read_file(const string &name) const {
    ifstream in{ name };
    // 一次性读取整个文件内容,并将其作为单个字符串返回
    return { istreambuf_iterator<char>{ in },
            istreambuf_iterator<char>{ } };
  }
  bool is_valid_content(string_view) const;
public:
  thing create_thing_from(const string &name) const {
    auto contents = read_file(name);
    if(!is_valid_content(contents))
      throw invalid_format{};
    // 注意:此处调用了 thing 的私有构造函数
    return { contents };
  }
}

我们并不希望所有人都能调用,那个接受任意 string_view 参数的私有构造函数 thing,因为该构造函数的初衷并不是处理那些未经验证的字符字符串。正因如此,我们只允许 thing_factory 使用它,从而增强了封装性。

发布代码时,通常会将一个类及其友元放在一起,它们是紧密相关的:本质上,一个类的友元是该类接口的外部扩展。最后,友元关系有一些限制。友元关系不是相互的;如果类 A 将类 B 声明为自己的友元,并不代表类 B 也会将类 A 视为友元。例如:

class A {
  int n = 3;
  friend class B;
public:
  void f(B);
};

class B {
  int m = 4;
public:
  void f(A);
};

void A::f(B b) {
  // int val = b.m; // 错误:A 不是 B 的友元,无法访问 B 的私有成员 m
}

void B::f(A a) {
  int val = a.n; // 正确:B 是 A 的友元类,可以访问 A 的私有成员 n
}

友元关系不具有传递性;如果类 A 将类 B 声明为友元,而类 B 又将类 C 声明为友元,这并不代表类 A 也将类 C 视为友元:

class A {
  int n = 3;
  friend class B;
};

class B {
  friend class C;
public:
  void f(A a) {
    int val = a.n; // 正确:B 是 A 的友元,可以访问 A 的私有成员 n
  }
};

class C {
public:
  void f(A a) {
  // int val = a.n; // 错误:C 不是 A 的友元,无法访问 A 的私有成员 n
  }
}

最后但同样重要的是,友元关系不会继承;如果类 A 将类 B 声明为友元,并不代表当 C 是 B 的派生类时,A 也将 C 视为友元:

class A {
  int n = 3;
  friend class B;
};

class B {
public:
  void f(A a) {
    int val = a.n; // 正确:B 是 A 的友元,可以访问 A 的私有成员 n
  }
};

class C : B {
public:
  void f(A a) {
    // int val = a.n; // 错误:C 不是 A 的友元,无法访问 A 的私有成员 n
  }
}

如果使用得当,friend 能够解决一些在其他情况下难以处理的封装问题,是一种强大而灵活的机制。