在C++中,何时应该使用'friend'?

397

我一直在阅读 C++ FAQ,对 friend 声明很好奇。我个人从未使用过它,但我有兴趣探索这门语言。

有没有使用 friend 的好例子?


继续阅读FAQ,我喜欢重载 << >> 操作符并将其添加为那些类的友元的想法。但我不确定如何做到不破坏封装性。这些例外情况在面向对象编程(OOP)的严格性范围内什么时候可以保留?


6
虽然我同意这个答案,即友元类不一定是一个坏事,但我倾向于将它视为代码小问题。它通常(虽然不总是)表明类层次结构需要重新考虑。 - Mawg says reinstate Monica
1
如果已经存在紧密耦合,您将使用友元类。这就是它的用途。例如,数据库表及其索引是紧密耦合的。当表更改时,必须更新其所有索引。因此,类DBIndex会声明DBTable为友元,以便DBTable可以直接访问索引内部。但是,对于DBIndex没有公共接口;甚至读取索引也没有意义。 - shawnhcorey
2
面向对象编程(OOP)的“纯粹主义者”认为友元违反了OOP原则,因为一个类应该是其私有状态的唯一维护者。这很好,直到你遇到两个类需要维护共享私有状态的常见情况。 - kaalus
31个回答

369
首先(在我看来),不要听那些说“朋友”无用的人。它是有用的。在许多情况下,您将拥有具有数据或功能的对象,这些数据或功能不打算公开使用。这一点尤其适用于具有许多作者并且可能只对不同区域略有了解的大型代码库。
虽然有替代友元指定符的方法,但通常它们很麻烦(cpp级别的具体类/掩蔽typedef)或者不太完美(注释或函数名称约定)。
关于答案:
友元指定符允许指定的类访问在进行友元声明的类中受保护的数据或功能。例如,在下面的代码中,任何人都可以询问孩子的名字,但只有母亲和孩子才能更改名字。
您可以通过考虑更复杂的类(如Window)来进一步简化此示例。很可能一个Window将有许多不应该公开访问但需要由相关类(如WindowManager)使用的函数/数据元素。
class Child
{
//Mother class members can access the private parts of class Child.
friend class Mother;

public:

  string name( void );

protected:

  void setName( string newName );
};

129
额外说明,C++ FAQ提到friend能够增强封装性。friend向成员授予 有选择性的访问权限,就像protected一样。任何细粒度的控制都比公开访问好。其他语言也定义了有选择性的访问机制,例如考虑C#的internal。关于使用friend的大部分负面批评与更紧密的耦合有关,这通常被认为是不好的。然而,在某些情况下,更紧密的耦合恰恰是您想要的,而friend正是给了您这种权力。 - André Caron
6
请您进一步说明一下(cpp-level concrete classes)和(masked typedefs)是什么意思,这是来自Andrew的问题。 - OmarOthman
26
这个回答似乎更侧重于解释“friend”是什么,而不是提供一个有启发性的例子。Window/WindowManager的例子比展示的例子要好,但太过模糊。这个回答还没有涉及到封装问题。 - bames53
4
因为C++没有一个包概念,所有成员可以共享实现细节,所以'friend'的存在是有效的。我会很感兴趣听一个真实世界的例子。 - weberc2
2
拥有朋友总是很好的事情;没有他们,你将会孤独无助!我们共同面对这一切,然而有些事情应该保持私密(我们的隐私),而其他事情则可以与朋友分享。 - Francis Cugler
显示剩余13条评论

175

在工作中,我们广泛地使用“朋友用于测试代码”的技术。这意味着我们可以为主要的应用程序代码提供适当的封装和信息隐藏。同时,我们可以编写独立的测试代码,使用“朋友”来检查内部状态和数据以进行测试。

可以说,我不会将“朋友关键字”作为设计的基本组成部分。


17
个人而言,我会不鼓励这样做。通常你测试的是一个接口,也就是一组输入是否能产生预期的输出结果。为什么需要检查内部数据呢? - Graeme
64
@Graeme:因为一个好的测试计划包括白盒测试和黑盒测试。 - Ben Voigt
3
我倾向于同意@Graeme的观点,正如这个答案中所完美解释的那样。 - Alexis Leclerc
3
@Graeme,可能不直接是内部数据。它可能是执行特定操作或任务的方法,在该方法为类私有且不应公开访问的情况下,某些其他对象可能需要使用自己的数据来提供或填充该类的受保护方法。 - Francis Cugler
1
这就是为什么没有人想和程序员交朋友的原因。 - GamefanA
显示剩余2条评论

105
friend关键字有很多好处。这里是我立即看到的两个用途:

友元定义

友元定义允许在类作用域中定义一个函数,但该函数不会被定义为成员函数,而是作为封闭命名空间的自由函数定义,并且通常不可见,除非是参数依赖查找。这使其特别适用于运算符重载:

namespace utils {
    class f {
    private:
        typedef int int_type;
        int_type value;

    public:
        // let's assume it doesn't only need .value, but some
        // internal stuff.
        friend f operator+(f const& a, f const& b) {
            // name resolution finds names in class-scope. 
            // int_type is visible here.
            return f(a.value + b.value);
        }

        int getValue() const { return value; }
    };
}

int main() {
    utils::f a, b;
    std::cout << (a + b).getValue(); // valid
}

私有CRTP基类

有时,您会发现需要一个策略可以访问派生类:

// possible policy used for flexible-class.
template<typename Derived>
struct Policy {
    void doSomething() {
        // casting this to Derived* requires us to see that we are a 
        // base-class of Derived.
        some_type const& t = static_cast<Derived*>(this)->getSomething();
    }
};

// note, derived privately
template<template<typename> class SomePolicy>
struct FlexibleClass : private SomePolicy<FlexibleClass> {
    // we derive privately, so the base-class wouldn't notice that, 
    // (even though it's the base itself!), so we need a friend declaration
    // to make the base a friend of us.
    friend class SomePolicy<FlexibleClass>;

    void doStuff() {
         // calls doSomething of the policy
         this->doSomething();
    }

    // will return useful information
    some_type getSomething();
};

您可以在这个答案中找到一个非人为的例子。另一个使用该代码的示例在这个答案中。CRTP基类强制转换其this指针,以便使用数据成员指针访问派生类的数据字段。

嗨,当我尝试使用你的CRTP时(在xcode 4中),我遇到了语法错误。Xcode认为我正在尝试继承一个类模板。错误发生在template<template<typename> class P> class C:P<C> {};中的P<C>处,指出“需要使用类模板C的模板参数”。你是否遇到过同样的问题或者知道解决方案? - bennedich
@bennedich 乍一看,这似乎是由于 C++ 特性支持不足而导致的错误。这在编译器中非常普遍。在 FlexibleClass 中使用 FlexibleClass 应该隐式地引用其自身类型。 - Yakk - Adam Nevraumont
@bennedich:在C++11中,类模板名称在类体内的使用规则发生了变化。尝试在编译器中启用C++11模式。 - Ben Voigt
在Visual Studio 2015中添加以下公共内容: f() {}; f(int_type t):value(t) {}; 以防止此编译器错误:error C2440: '<function-style-cast>': cannot convert from 'utils::f::int_type' to 'utils::f' note: No constructor could take the source type, or constructor overload resolution was ambiguous. - Damian
使用gcc 4.8和-std=c++11以及gcc 5.1和-std=c++11,-std=c++14和-std=c++17,每次都会导致以下错误: error: type/value mismatch at argument 1 in template parameter list for 'template<class> class SomePolicy' struct FlexibleClass : private SomePolicy<FlexibleClass> ^ crtp.cpp:17:56: note: expected a type, got 'FlexibleClass' 使用gcc真的可以进行这种模板化的CRTP吗? (请注意,我在开头添加了using some_type = int; - Maitre Bart

46

@roo:封装在这里并没有被破坏,因为类本身控制着谁可以访问其私有成员。只有在这可以从类外部引起时,例如如果您的operator<<宣布“我是类foo的朋友”,封装才会被破坏。

friend替换了public的使用,而不是private的使用!

实际上,C++ FAQ已经回答了这个问题


16
“朋友代替公共场合,而不是代替私人场所!”我赞成。 - Waleed Eissa
26
@Assaf: 是的,但是大部分 FQA 都是一些毫无价值、支离破碎的愤怒胡言,关于 friend 的部分也不例外。这里唯一真正的观察是 C++ 仅在编译时保证了封装性。而你并不需要更多的话来表达它。其余的都是废话。因此,总之:FQA 中的这个部分不值得一提。 - Konrad Rudolph
12
那个FQA中大部分都是胡说八道的 :) - rama-jka toti
1
@Konrad:“这里唯一真正的观察是,C++仅在编译时确保封装。”有没有语言可以在运行时确保这一点?据我所知,在C#,Java,Python和许多其他语言中,返回对私有成员(以及函数,对于允许指向函数或将函数作为第一类对象的语言)的引用是被允许的。 - André Caron
“@AndréCaron”:“_有哪些语言可以在运行时确保这一点?_”任何具有强反射能力和旨在“类型安全”的语言都应该在运行时检查封装! - curiousguy
显示剩余5条评论

32

经典的例子是重载 operator<<。另一个常见用法是允许帮助类或管理员类访问您的内部。

以下是我听说过的一些 C++ 友元函数指导方针。最后一条尤其令人难忘。

  • 你的朋友不是你孩子的朋友。
  • 你孩子的朋友不是你的朋友。
  • 只有友元才能触及你的私有部位。

"经典示例是重载operator<<。我猜是不使用“friend”的经典示例。" - curiousguy

24

编辑:阅读 faq 更久一点,我喜欢重载运算符 <<、>> 并将其作为这些类的友元的想法,但我不确定如何不破坏封装

它会如何破坏封装?

当您允许对数据成员的 不受限制 访问时,您就会破坏封装。考虑以下类:

class c1 {
public:
  int x;
};

class c2 {
public:
  int foo();
private:
  int x;
};

class c3 {
  friend int foo();
private:
  int x;
};

c1 显然没有封装。任何人都可以读取和修改其中的 x。我们无法强制执行任何形式的访问控制。

c2 显然被封装了。没有公共访问 x 的方式。你能做的就是调用 foo 函数,它对该类执行一些有意义的操作。

c3?它的封装性是否更差?它允许无限制地访问 x 吗?它允许未知函数访问吗?

不是的。它只允许一个函数访问类的私有成员。就像 c2 一样。而且同样具有访问权限的函数并不是“一些随机的、未知的函数”,而是“在类定义中列出的函数”。就像 c2 一样,我们可以通过查看类定义来看到完整的访问权限列表。

那么它究竟如何比封装性更差呢?同样的代码可以访问类的私有成员。而且每个有访问权限的人都在类定义中列出。

friend 并不会破坏封装性。它让一些 Java 程序员感到不舒服,因为当他们说“面向对象编程”时,实际上是指“Java”。“封装”并不意味着“必须保护私有成员免于受到任意访问”,而是“只有类成员能够访问私有成员的 Java 类”,即使这对多个原因来说都是完全无稽之谈。

首先,正如已经显示的那样,这种方法过于限制性。没有理由不允许 friend 方法执行相同操作。

其次,它的限制不够。考虑第四个类:

class c4 {
public:
  int getx();
  void setx(int x);
private:
  int x;
};

根据上述Java思想,这是完全封装的。 然而,它允许任何人读取和修改x。这怎么说得通?(提示:并不能)

底线: 封装是关于控制哪些函数可以访问私有成员。它与这些函数的定义准确位置无关。不是关于函数定义的位置。


点赞,因为我喜欢你大部分的观点和底线陈述。然而,我认为你最后一个例子是不准确的。空的getter和setter并不是用来限制访问的,它们的目的是允许在未来添加封装功能而不会破坏类API。即使在这个例子中,x 完全封装的。你已经隐藏了内部表示并限制了对x变量的直接访问。借用你优秀的措辞,封装是关于隐藏和限制对内部表示的访问,而不是改变它。 - dallin

10

安德鲁例子中的另一种常见形式是可怕的代码对

parent.addChild(child);
child.setParent(parent);

不必担心两行代码是否总是一起执行以及执行顺序是否一致,您可以将方法设置为私有,并使用友元函数来强制执行一致性:

class Parent;

class Object {
private:
    void setParent(Parent&);

    friend void addChild(Parent& parent, Object& child);
};

class Parent : public Object {
private:
     void addChild(Object& child);

     friend void addChild(Parent& parent, Object& child);
};

void addChild(Parent& parent, Object& child) {
    if( &parent == &child ){ 
        wetPants(); 
    }
    parent.addChild(child);
    child.setParent(parent);
}

换句话说,您可以保持公共接口更小,并通过友元函数强制执行跨类和对象的不变量。

6
为什么有人需要朋友来做那件事?为什么不让 addChild 成员函数也设置父节点呢? - Nawaz
1
一个更好的例子是将 setParent 声明为友元函数,因为你不希望客户端改变父对象,因为这个操作应该在 addChild/removeChild 函数中进行管理。 - Ylisar

8
我发现一个方便的场合可以使用友元访问:私有函数的单元测试。

但是公共函数也可以用于此吗?使用友元访问的优点是什么? - Zheng Qu
@Maverobot,您能详细说明您的问题吗? - VladimirS

8
你可以使用Private/Protected/Public控制成员和函数的访问权限,假设这3个级别的概念都很清楚,那么很明显我们缺少了一些东西...例如,将成员/函数声明为protected就太普遍了。你是在说除了继承子类之外,每个人都无法访问该函数。但是除外情况呢?每个安全系统都允许你有某种类型的“白名单”吧?
因此,friend允许您拥有坚固的对象隔离性,但也允许创建“漏洞”,以用于您认为合理的事情。
我想人们之所以说它不需要是因为总有一种设计可以没有它。我认为这类似于全局变量的讨论:您永远不应该使用它们,总有一种方法可以没有它们...但实际上,您会看到它们几乎是最优雅的方式...我觉得这也适用于友元。
引用:“它实际上并没有好处,除了让您可以使用成员变量而不使用设置函数”
哦,这并不是正确的看待方法。 重点是控制谁能访问什么,是否有设置函数与此无关。

2
“friend” 如何成为漏洞?它允许类中列出的方法访问其私有成员。但它仍然不允许任意代码访问它们。因此,它与公共成员函数没有区别。 - jalf
朋友类是在C++中最接近C#/Java包级访问的方式。@jalf - 那么友元类呢(例如工厂类)? - Ogre Psalm33
1
@Ogre:它们怎么样?你仍然只是特别地给了那个类而不是其他人访问这个类内部的权限。你不仅仅是为了让任意未知代码来随意干扰你的类而打开门。 - jalf

6
C++ 的创造者表示,这不会违反任何封装原则,我引用他的话:“友元”并不违反封装性。它是一种明确授权访问的机制,就像成员一样。你不能(在符合标准的程序中)在不修改类源代码的情况下获得对其的访问权限。这应该很清楚了。

@curiousguy:即使在模板的情况下,这也是正确的。 - Nawaz
@Nawaz 模板友元可以被授予,但任何人都可以创建新的部分或显式特化,而不必修改授予友元的类。但是在这样做时要小心 ODR 违规。无论如何也不要这样做。 - curiousguy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接