为什么C ++不允许继承友谊?


93

为什么友谊在C ++中至少不能有选择地继承?我知道出于明显的原因而禁止传递性和自反性(我说这只是为了避开简单的FAQ引用答案),但是缺少一些东西使virtual friend class Foo;我感到困惑。有人知道这个决定背后的历史背景吗?友谊真的只是一种有限的黑客手段,此后又进入了一些晦涩可敬的用途吗?

编辑以澄清问题:我是在谈论以下情况,而不是 A的孩子接触B或B及其孩子都接触的情况。我也可以想象有选择地授予对朋友功能的覆盖等的访问权限。

class A {
  int x;
  friend class B;
};

class B {
  // OK as per friend declaration above.
  void foo(A& a, int n) { a.x = n; }
};

class D : public B { /* can't get in A w/o 'friend class D' declaration. */ };

接受的答案:正如Loki所说,可以通过在友善的基类中创建受保护的代理函数来或多或少地模拟效果,因此,并不需要严格地将友谊授予类或虚拟方法层次结构。我不喜欢样板代理(友好的基础实际上变成了代理),但是我认为这比语言机制更可取,后者在大多数情况下很可能会被滥用。我认为可能是我购买和阅读Stroupstrup的《The Design and Evolution of C ++》(我已经在这里看到足够多的人推荐)的时候,才能更好地了解这些类型的问题……

Answers:


93

因为我可能写信Foo及其朋友Bar(因此存在信任关系)。

但是,我是否相信编写派生自此类的类的人Bar
并不是的。因此,他们不应该继承友谊。

类的内部表示形式的任何更改都将需要修改任何依赖于该表示形式的内容。因此,班上的所有成员以及班上的所有朋友都将需要修改。

因此,如果的内部表示Foo被修改,则Bar还必须进行修改(因为友谊紧密地绑定BarFoo)。如果友谊是继承的,则所有派生自的类Bar也将紧密地绑定在一起Foo,因此,如果Foo更改了内部表示,则需要进行修改。但是我不知道派生类型(我也不应该。它们甚至可能由其他公司开发)。因此,我将无法进行更改,Foo因为这样做会在代码库中引入重大更改(因为我无法修改从派生的所有类Bar)。

因此,如果继承了友谊,那么您会无意中对修改类的能力施加了限制。这是不希望有的,因为您基本上使公共API的概念变得无用。

注意:的子项Bar可以Foo通过使用Bar进行访问,只需将方法设置为Bar保护。然后,的孩子Bar可以Foo通过其父类进行调用来访问。

这是你想要的吗?

class A
{
    int x;
    friend class B;
};

class B
{
    protected:
       // Now children of B can access foo
       void foo(A& a, int n) { a.x = n; }
};

class D : public B
{
    public:
        foo(A& a, int n)
        {
            B::foo(a, n + 5);
        }
};

4
答对了。所有这些都是为了限制您可以通过更改类的内部而造成的损害。
j_random_hacker 2010年

坦白地说,我真正考虑的情况是“律师-客户”模式,其中中间层通过向基础的受限访问类提供包装方法来充当外部类的受限接口。说一个接口对其他类的所有子类都可用而不是一个确切的类,将比当前的系统有用得多。
杰夫

@Jeff:将内部表示形式暴露给类的所有子代将导致代码不可更改(它实际上也破坏了封装,因为任何想要访问内部成员的人都必须从Bar继承,即使他们不是真正的Bar。 )。
马丁·约克2010年

@马丁:是的,在这种方案中,可以使用友善的基础来获得友善的类,在很多(如果不是大多数)情况下,这可能是简单的封装违规。但是,在友好基础是抽象类的情况下,任何派生类都将被强制实现自己的对等接口。我不确定在这种情况下,如果“ impostor”类没有尝试忠实地履行其声明的角色,是否会认为它破坏封装或违反接口合同。
杰夫

@Martin:是的,这就是我想要的效果,有时已经实际使用了,其中A实际上是对某些受限访问类Z的相互友好的接口。通常的Attorney-Client Idiom常见的抱怨似乎是该接口类A必须将调用包装器样板化为Z,并且为了扩展对子类的访问,A的样板件必须在每个基本类(如B)中基本重复。接口通常表示模块要提供的功能,而不是模块中要提供的功能。想使用自己。
杰夫(Jeff)2010年

48

为什么友谊至少不能有选择地在C ++中继承?

我认为,第一个问题的答案是这个问题:“您父亲的朋友可以使用您的私人物品吗?”


36
公平地说,这个问题引起了关于你父亲的不安。。。
iheanyi 2015年

3
这个答案有什么意义?充其量只是一个可疑但可能轻松的评论
DeveloperChris

11

友好的类可以通过访问器函数公开其朋友,然后通过这些访问器授予访问权限。

class stingy {
    int pennies;
    friend class hot_girl;
};

class hot_girl {
public:
    stingy *bf;

    int &get_cash( stingy &x = *bf ) { return x.pennies; }
};

class moocher {
public: // moocher can access stingy's pennies despite not being a friend
    int &get_cash( hot_girl &x ) { return x.get_cash(); }
};

这比可选的传递性允许更好的控制。例如,get_cash可以是protected或可以强制执行运行时受限访问的协议。


@Hector:投票支持重构! ;)
亚历山大·舒卡耶夫

7

C ++标准,第11.4 / 8节

友谊既不是继承也不是传递。

如果将继承友谊,那么原本不打算成为朋友的类将突然可以访问您的类内部,并且违反了封装。


2
假设Q是“朋友” A,而B是从A派生的。如果B继承了A的友谊,那么由于B是A的一种,因此从技术上讲,它是A,可以访问Q的私人。因此,这没有任何实际原因来回答问题。
mdenton8 2015年

2

因为这是不必要的。

friend关键字的使用本身是可疑的。就耦合而言,这是最差的关系(在继承和组合之前)。

对班级内部进行任何更改都可能会影响该班级的朋友……您真的想要数量未知的朋友吗?如果从他们那里继承的人也可以成为朋友,那么您甚至无法列出他们,并且您冒着每次都破坏客户代码的风险,这肯定是不希望的。

我自由地承认,对于家庭作业/宠物项目,依赖通常是一个遥远的考虑。在小型项目中,这无关紧要。但是,一旦有几个人在同一个项目上工作,并且这个项目增长到成千上万的行,您就需要限制变更的影响。

这带来了一个非常简单的规则:

更改类的内部结构只会影响类本身

当然,您可能会影响它的朋友,但是这里有两种情况:

  • 免费的朋友功能:可能更多的是成员功能(我在std::ostream& operator<<(...)这里认为,这并不是纯粹出于语言规则的原因而成为的成员
  • 朋友班?您不需要在真实班级上参加朋友班。

我建议使用简单的方法:

class Example;

class ExampleKey { friend class Example; ExampleKey(); };

class Restricted
{
public:
  void forExampleOnly(int,int,ExampleKey const&);
};

这种简单的Key模式允许您在某种程度上声明一个朋友,而无需实际允许其访问您的内部,从而将其与更改隔离开来。此外,如果需要,它还允许该朋友将其密钥借给受托人(例如孩子)。


0

一个猜测:如果一个类将其他一些类/函数声明为朋友,那是因为第二个实体需要对第一个实体的特权访问。在向第二个实体授予对从第一个实体派生的任意数量的类的特权访问时,有什么用?


2
如果A类希望向B及其后代授予友谊,则可以避免为添加的每个子类更新其接口,也可以避免B编写直通样板,这首先是友谊的一半。
杰夫

@杰夫:啊,那我误会了你的意图。我以为您的意思是说,那B将可以访问所有继承自A...的类
奥利弗·查尔斯沃思

0

派生类只能继承基类的“成员”。朋友声明不是朋友类的成员。

$ 11.4 / 1-“ ...朋友的名称不在该类的范围内,除非该成员是另一个类的成员,否则不使用成员访问运算符(5.2.5)调用该朋友。”

$ 11.4-“而且,由于朋友阶层的基本条款不是其成员声明的一部分,因此朋友阶层的基本条款无法访问授予友谊的阶层的私有成员和受保护成员的姓名。”

并进一步

$ 10.3 / 7-“ [注:虚拟说明符暗含成员关系,因此虚拟函数不能是非成员(7.1.2)函数。虚拟函数也不能是静态成员,因为虚拟函数调用依赖于特定的对象确定要调用的函数。在一个类中声明的虚函数可以在另一个类中声明为好友。]“

由于“朋友”首先不是基类的成员,因此派生类如何继承它?


友谊虽然通过像成员这样的声明来提供,但实际上并不是真正的成员,而是关于其他类实际上可以忽略“真实”成员的可见性分类的通知。虽然您引用的规范部分解释了语言如何以自洽的术语来处理这些细微差别和框架行为,但事情本来可以以不同的方式进行,但是不幸的是,以上内容都没有成为基本原理的核心。
杰夫

0

类中的Friend函数将extern属性分配给该函数。即extern表示该函数已在类之外的某个地方声明和定义。

因此,这意味着friend function不是类的成员。因此,继承仅允许您继承类的属性,而不能继承外部事物。并且如果朋友函数允许继承,则第三方类也将继承。


0

朋友在继承方面表现出色,例如容器的样式接口。但是对我来说,正如我首先说的那样,C ++缺乏可传播的继承

class Thing;

//an interface for Thing container's
struct IThing {
   friend Thing;
   protected:
       int IThing_getData() = 0;
};

//container for thing's
struct MyContainer : public IThing {
    protected: //here is reserved access to Thing
         int IThing_getData() override {...}
};

struct Thing {
    void setYourContainer(IThing* aContainerOfThings) {
        //access to unique function in protected area 
        aContainerOfThings->IThing_getData(); //authorized access
    }
};

struct ChildThing : public Thing {
    void doTest() {
        //here the lack of granularity, you cannot access to the container.
        //to use the container, you must implement all 
        //function in the Thing class
        aContainerOfThings->IThing_getData(); //forbidden access
    }
};

对我来说,C ++的问题是缺少非常好的粒度来控制任何地方的所有访问:

朋友Thing可以成为朋友Thing。*,以授予对Thing的所有孩子的访问权限

还有,要授予精确访问权限的朋友[命名区域] Thing。*在容器类中,通过朋友的特殊命名区域。

好吧,停止梦想。但是现在,您知道了一个有趣的朋友用法。

在另一个顺序中,您还可以发现有趣的是已知的所有班级对自己都很友好。换句话说,一个类实例可以
不受限制地调用同名另一个实例的所有成员:

class Object {
     private:
         void test() {}
     protected:
         void callAnotherTest(Object* anotherObject) {
             //private, but yes you can call test() from 
             //another object instance
             anotherObject)->test(); 
         }
};

0

简单的逻辑:“我有一个朋友简。仅仅因为我们昨天成为朋友,并不能使她的所有朋友都成为我的朋友。”

我仍然需要批准那些个人的友谊,并且信任的程度也会相应提高。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.