¿Por qué C ++ no permite la amistad heredada?

93

¿Por qué la amistad no es al menos opcionalmente heredable en C ++? Entiendo que la transitividad y la reflexividad están prohibidas por razones obvias (digo esto solo para evitar respuestas simples de citas de preguntas frecuentes), pero la falta de algo en la línea de virtual friend class Foo;me desconcierta. ¿Alguien conoce los antecedentes históricos detrás de esta decisión? ¿Era la amistad realmente solo un truco limitado que desde entonces se ha abierto camino en algunos usos respetables oscuros?

Edite para aclarar: estoy hablando del siguiente escenario, no donde los hijos de A están expuestos a B o tanto a B como a sus hijos. También puedo imaginar la posibilidad de otorgar acceso a anulaciones de funciones de amigos, etc.

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. */ };

Respuesta aceptada: como dice Loki , el efecto se puede simular más o menos haciendo funciones proxy protegidas en clases base amigas, por lo que no hay una necesidad estricta de otorgar amistad a una jerarquía de clase o método virtual. No me gusta la necesidad de proxies repetitivos (en los que la base amiga se convierte efectivamente), pero supongo que esto se consideró preferible a un mecanismo de lenguaje que probablemente sería mal utilizado la mayor parte del tiempo. Creo que probablemente es hora de que compre y lea The Design and Evolution of C ++ de Stroupstrup , que he visto recomendar a suficientes personas aquí, para obtener una mejor comprensión de este tipo de preguntas ...

Jeff
fuente

Respuestas:

93

Porque puedo escribir Fooy su amigo Bar(por eso hay una relación de confianza).

¿Pero confío en las personas que escriben clases de las que se derivan Bar?
Realmente no. Entonces no deberían heredar la amistad.

Cualquier cambio en la representación interna de una clase requerirá una modificación a todo lo que dependa de esa representación. Por tanto, todos los miembros de una clase y también todos los amigos de la clase necesitarán modificaciones.

Por lo tanto, si la representación interna de Foose modifica, Bartambién debe modificarse (porque la amistad se une estrechamente Bara Foo). Si se heredara la amistad, todas las clases derivadas Bartambién estarían estrechamente vinculadas Fooy, por lo tanto, requerirían modificaciones si Foose cambia la representación interna. Pero no tengo conocimiento de los tipos derivados (ni debería hacerlo. Incluso pueden ser desarrollados por diferentes empresas, etc.). Por lo tanto, no podría cambiar, Fooya que hacerlo introduciría cambios importantes en la base del código (ya que no podría modificar todas las clases derivadas Bar).

Por lo tanto, si la amistad se heredó, inadvertidamente está introduciendo una restricción a la capacidad de modificar una clase. Esto no es deseable ya que básicamente hace inútil el concepto de API pública.

Nota: Un niño de Barpuede acceder Foousando Bar, simplemente haga que el método esté Barprotegido. Entonces el hijo de Barpuede acceder a Foollamando a través de su clase padre.

¿Es esto lo que quieres?

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);
        }
};
Loki Astari
fuente
4
Bingo. Se trata de limitar el daño que puedes causar cambiando los componentes internos de una clase.
j_random_hacker
Francamente, el caso en el que realmente estoy pensando es el patrón Abogado-Cliente, donde un intermedio actúa como una interfaz limitada para las clases externas al presentar métodos de envoltura a la clase subyacente de acceso restringido. Decir que una interfaz está disponible para todos los hijos de otras clases en lugar de una clase exacta sería mucho más útil que el sistema actual.
Jeff
@Jeff: Exponer la representación interna a todos los hijos de una clase hará que el código se vuelva inmutable (también rompe la encapsulación, ya que cualquiera que quisiera acceder a los miembros internos solo tiene que heredar de Bar incluso si no son realmente un Bar ).
Martin York
@Martin: Correcto, en este esquema, la base amiga podría usarse para llegar a la clase amiga, lo que podría ser una simple violación de encapsulación en muchos (si no en la mayoría) de los casos. Sin embargo, en situaciones en las que la base amiga es una clase abstracta, cualquier clase derivada se vería obligada a implementar una interfaz recíproca propia. No estoy seguro de si una clase de 'impostor' en este escenario se consideraría que está rompiendo la encapsulación o que está violando un contrato de interfaz si no tratara de desempeñar fielmente su función reclamada correctamente.
Jeff
@Martin: Correcto, ese es el efecto que quiero y, a veces, de hecho ya lo uso, donde A es en realidad una interfaz de amistad recíproca para alguna clase de acceso restringido Z. La queja común con el lenguaje abogado-cliente normal parece ser que la clase de interfaz A tiene que convertir los envoltorios de llamadas a Z, y para extender el acceso a las subclases, el texto estándar de A tiene que estar esencialmente duplicado en cada clase base como B.Una interfaz normalmente expresa qué funcionalidad quiere ofrecer un módulo, no qué funcionalidad en otros. quiere usarse a sí mismo.
Jeff
48

¿Por qué la amistad no es al menos opcionalmente heredable en C ++?

Creo que la respuesta a tu primera pregunta está en esta pregunta: "¿Los amigos de tu padre tienen acceso a tus partes privadas?"

Wilx
fuente
36
Para ser justos, esa pregunta suscita inquietudes sobre tu padre. . .
iheanyi
3
¿Cuál es el punto de esta respuesta? en el mejor de los casos, un comentario dudoso, aunque posiblemente alegre
DeveloperChris
11

Una clase amiga puede exponer a su amigo a través de funciones de acceso y luego otorgar acceso a través de ellas.

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(); }
};

Esto permite un control más fino que la transitividad opcional. Por ejemplo, get_cashpuede ser protectedo puede imponer un protocolo de acceso limitado en tiempo de ejecución.

Potatoswatter
fuente
@Hector: ¡Votando por la refactorización! ;)
Alexander Shukaev
7

Estándar C ++, sección 11.4 / 8

La amistad no se hereda ni es transitiva.

Si se heredara la amistad, entonces una clase que no estaba destinada a ser un amigo de repente tendría acceso a los aspectos internos de su clase y eso violaría la encapsulación.

David
fuente
2
Diga Q "amigos" A, y B se deriva de A. Si B hereda la amistad de A, entonces, debido a que B es un tipo de A, técnicamente ES una A que tiene acceso a las partes privadas de Q. Entonces esto no responde a la pregunta con ninguna razón práctica.
mdenton8
2

Porque es simplemente innecesario.

El uso de la friend palabra clave es en sí mismo sospechoso. En términos de acoplamiento, es la peor relación (muy por delante de la herencia y la composición).

Cualquier cambio en el interior de una clase tiene el riesgo de afectar a los amigos de esta clase ... ¿de verdad quieres un número desconocido de amigos? Ni siquiera podrías enumerarlos si quienes heredan de ellos pudieran ser amigos también, y corres el riesgo de romper el código de tus clientes cada vez, seguramente esto no es deseable.

Admito libremente que para los proyectos de tareas o mascotas, la dependencia es a menudo una consideración lejana. En proyectos de pequeño tamaño, no importa. Pero tan pronto como varias personas trabajen en el mismo proyecto y este se convierta en decenas de miles de líneas, deberá limitar el impacto de los cambios.

Esto trae una regla muy simple:

Cambiar las partes internas de una clase solo debería afectar a la clase misma

Por supuesto, probablemente afectará a sus amigos, pero aquí hay dos casos:

  • función libre de amigos: probablemente más una función de miembro de todos modos (creo que std::ostream& operator<<(...) aquí, que no es un miembro simplemente por accidente de las reglas del idioma
  • clase de amigo? no necesitas clases de amigos en clases reales.

Recomendaría el uso del método simple:

class Example;

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

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

Este Keypatrón simple le permite declarar a un amigo (de alguna manera) sin realmente darle acceso a sus partes internas, aislándolo de los cambios. Además, le permite a este amigo prestar su llave a los fideicomisarios (como niños) si es necesario.

Matthieu M.
fuente
0

Una suposición: si una clase declara alguna otra clase / función como amiga, es porque esa segunda entidad necesita acceso privilegiado a la primera. ¿De qué sirve conceder acceso privilegiado a la segunda entidad a un número arbitrario de clases derivadas de la primera?

Oliver Charlesworth
fuente
2
Si una clase A quisiera otorgar amistad a B y sus descendientes, podría evitar actualizar su interfaz para cada subclase agregada u obligar a B a escribir un texto repetitivo de paso, que es la mitad del punto de amistad en primer lugar, creo.
Jeff
@Jeff: Ah, entonces entendí mal el significado que pretendías. Supuse que BA
Oliver Charlesworth
0

Una clase derivada puede heredar solo algo, que es 'miembro' de la base. Una declaración de amistad no es un miembro de la clase de amistad.

$ 11.4 / 1- "... El nombre de un amigo no está en el alcance de la clase, y el amigo no se llama con los operadores de acceso de miembros (5.2.5) a menos que sea miembro de otra clase".

$ 11.4 - "Además, debido a que la cláusula base de la clase friend no es parte de sus declaraciones de miembros, la cláusula base de la clase friend no puede acceder a los nombres de los miembros privados y protegidos de la clase que otorga amistad".

y además

$ 10.3 / 7- "[Nota: el especificador virtual implica membresía, por lo que una función virtual no puede ser una función que no sea miembro (7.1.2). Tampoco una función virtual puede ser un miembro estático, ya que una llamada de función virtual se basa en un objeto específico para determinar qué función invocar. Una función virtual declarada en una clase puede ser declarada amiga en otra clase.] "

Dado que el 'amigo' no es miembro de la clase base en primer lugar, ¿cómo puede ser heredado por la clase derivada?

Chubsdad
fuente
La amistad, aunque se da a través de declaraciones como miembros, en realidad no son miembros tanto como notificaciones de que otras clases esencialmente pueden ignorar las clasificaciones de visibilidad de los miembros "reales". Si bien las secciones de especificaciones que cita explican cómo funciona el lenguaje con respecto a estos matices y encuadran el comportamiento en una terminología coherente, las cosas podrían haberse elaborado de manera diferente y, lamentablemente, nada de lo anterior llega al corazón de la razón fundamental.
Jeff
0

La función de amigo en una clase asigna la propiedad extern a la función. es decir, extern significa que la función ha sido declarada y definida en algún lugar fuera de la clase.

Por lo tanto, significa que la función de amigo no es miembro de una clase. Entonces, la herencia solo te permite heredar las propiedades de una clase, no cosas externas. Y también si se permite la herencia para funciones de amigos, entonces una clase de terceros hereda.

Robert
fuente
0

Friend es bueno en herencia como interfaz de estilo para contenedor Pero para mí, como los primeros dicen, C ++ carece de la herencia propagable

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
    }
};

Para mí, el problema de C ++ es la falta de una granularidad muy buena para controlar todos los accesos desde cualquier lugar para cualquier cosa:

Friend Thing puede convertirse en friend Thing. * para otorgar acceso a todos los hijos de Thing

Y más, amigo [área con nombre] Cosa. * Para otorgar acceso a una ubicación precisa en la clase Contenedor a través del área con nombre especial para el amigo.

Ok, detén el sueño. Pero ahora, conoces un uso interesante de amigo.

En otro orden, también puede resultar interesante saber que todas las clases son amistosas consigo mismas. En otras palabras, una instancia de clase puede llamar a todos los
miembros de otra instancia del mismo nombre sin restricción:

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

Lógica simple: 'Tengo una amiga Jane. El hecho de que nos hiciéramos amigos ayer no hace que todos sus amigos sean míos.

Todavía necesito aprobar esas amistades individuales, y el nivel de confianza sería el correspondiente.

Robert Hamm
fuente