Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.11.2a1  Clases y funciones friend

§1  Sinopsis

Se ha señalado ( 4.11.2a), que el mecanismo ofrecido por los especificadores de acceso public, private y protected, junto con alguna matización, que veremos al referirnos a los especificadores de acceso en la herencia ( 4.11.2b), permiten mantener una cierta separación (encapsulamiento) entre la implementación de una clase y su interfaz.

Lo normal es que las propiedades privadas sean accedidas de forma controlada a través de métodos públicos.  Sin embargo, la invocación a funciones tiene su costo ( 4.4.6b), es mucho más eficiente acceder directamente a las propiedades.  En ocasiones es necesario acceder a miembros de una clase desde otra o desde muchas otras. Podemos resumir diciendo que el encapsulamiento, pese a sus innegables ventajas, es un artificio demasiado rígido para ciertas situaciones, por lo que se ha previsto un mecanismo que en cierta forma permite "saltárselo".  Se establece mediante la palabra-clave friend que puede aplicarse de forma global a clases y de forma individual a funciones; incluyendo entre estas últimas los métodos (funciones dentro de clases).

§2  friend (palabra-clave)

La palabra-clave friend es un especificador de acceso.  Se usa para declarar que una función o clase (la denominaremos "invitada"), tiene derecho de acceso a los miembros de la clase en que se declara (denominada "anfitriona"), sin que la clase invitada tenga que ser miembro de la anfitriona. En todos los otros aspectos la invitada es una función o clase normal en lo que se refiere a su declaración, definición y ámbito.

Dicho en otras palabras:  friend es un especificador de ámbito que "alarga" el ámbito del invitado (clase o función) al interior de la clase anfitriona  (a todo el interior, incluyendo miembros privados y protegidos !!).

§3  Sintaxis

friend <identificador>;

Si <identificador> es una función, desde esta puede accederse a los miembros privados y protegidos de la clase anfitriona.  Ejemplo:

class C {
  friend void foo(C&);
  int x;           // privada por defecto
  ...
};
void foo(C& c) { cout << c.x << endl; }  // Ok.


Si <identificador> es una clase, los miembros privados y protegidos de la clase anfitriona pueden ser accedidos desde todas las funciones miembro de la invitada.  Ejemplo:

class C {
  friend class D;
  int x;           // privada por defecto
};
class D {
  int y;
  public:
  void getx(C c) { cout << c.x << endl; }  // Ok.
  D(C& c) { y = c.x; }                     // Ok.
};

§4  Ejemplo-1


#include <iostream>
using namespace std;

class A {                  // clase anfitriona
  char c;                  // privado por defecto
  static int sti;
  char getC(void);         // función privada
  public:
  friend void finv(A* aptr, char ch );  // L.9: finv es función invitada
  void func(A* aptr, char ch);          // L.10: func es función miembro
};

int A::sti = 33;                     // inicia miembro estático
char A::getC(void) { return c; }     // define función privada

void finv (A* aptr, char ch) {       // L.16: define función
  aptr->c = ch;
  cout << "valor de c: " << aptr->getC() << endl;
  cout << "valor de sti: " << A::sti << endl;
}

void A::func (A* aptr, char ch) {    // L.22: define función miembro
  aptr->c = ch;
  cout << "valor de c: " << aptr->getC() << endl;
  cout << "valor de sti: " << A::sti << endl;
}

int main() {           // =========================
  A a1;                // instancia A
  finv(&a1, 'Y');      // L.30: invocar función
  a1.func(&a1, 'Z');   // L.31: invocar función-miembro
  return 0;
}

Salida:

valor de c: Y
valor de sti: 33
valor de c: Z
valor de sti: 33

Comentario

En este ejemplo, la clase anfitriona, A, que tiene tres miembros privados (dos propiedades y un método), declara como friend a la función finv, lo que garantiza que desde esta se pueda acceder a los miembros de A, tanto los públicos como los privados: c, sti, getC y func.

Observe que aunque las funciones finv y func hacen prácticamente lo mismo y tienen definiciones idénticas; la primera es una función normal (observe su definición en L.16 y la invocación en L.30), mientras que func es una función miembro de A (como lo atestigua su definición en L.22 y su invocación en L.31). Puede comprobarse como, a pesar de ser externa a la clase, la función finv puede acceder a sus propiedades y métodos incluso privados.

Es de señalar como el acceso al miembro estático sti, puede hacerse (L.19 y L.25) sin utilizar ningún objeto ( 4.11.7), mientras que el acceso a miembros normales como c en L.17 y L.23 debe hacerse refiriéndose a un objeto concreto (señalado en este caso por el puntero aptr).

§5  Ejemplo-2

Completamos el ejemplo anterior con una versión el la que en vez de ser una función, el "invitado" es una clase. Vemos como sus miembros (dos funciones) tienen acceso a los miembros, incluso privados, de la clase anfitriona:


#include <iostream>
using namespace std;

class A {                        // clase anfitriona
  char c;                        // privado por defecto
  char getC(void);               // función privada
  static int sti;                // miembro estático
  public:
  friend class B;                // L.9: declara B clase invitada
};
int A::sti = 33;                 // inicia miembro estático
char A::getC(void) { return c; } // define función privada

class B {                        // L.14: define clase invitada
  public:
  void setAc(class A&, char);    // método-1
  static void getA(class A*);    // método-2
};
void B::setAc(class A& aref, char ch) {    // L.19 define método-1
  aref.c = ch;
}
void B::getA(class A* aptr) {    // L.22 define método-2
  cout << "valor de c: " << aptr->getC() << endl;
  cout << "valor de sti: " << A::sti << endl;
}

int main() {              // =========================
  A a1;                   // L.28: instancia A
  B b1;                   // instancia B
  A& aref = a1;           // define referencia de clase A
  b1.setAc(aref, 'Y');    // L.31: invoca método público de objeto
  b1.getA(&a1);
  B::getA(&a1);
  return 0;
}

Salida:

valor de c: Y
valor de sti: 33
valor de c: Y
valor de sti: 33

Comentario

En este ejemplo, la clase anfitriona A tiene una clase amiga (B) que se declara en L.9.  Esta clase está definida en L.14/L.26, donde vemos que solo tiene dos métodos públicos.  El primero (setAc) permite establecer el valor de la propiedad c de un objeto de la clase anfitriona A; el acceso se realiza mediante una referencia.

El segundo de los métodos (getA) permite obtener los valores de las dos propiedades privadas de un objeto, aunque en este caso el acceso lo realizamos mediante un puntero.  Vemos como el acceso a la propiedad c se realiza primero directamente (L.20), para acto seguido accederla a través de un método (también privado) en L.23.

La función main se limita a crear los objetos y la referencia que serán utilizadas a continuación para invocar los métodos correspondientes de la clase invitada (L.31, 32 y 32).  Las invocaciones de L.32 y L.32 son equivalentes. En L.32 utilizamos la notación tradicional de invocación de método de objeto; en L.33 aprovechamos la característica de que getA es estática y utilizamos la notación específica que solo está permitida con este tipo de funciones miembro ( 4.11.7).

§6  La invitación puede ser restringida

La invitación no tiene porqué ser a todos los miembros de una clase; puede restringirse a uno de sus métodos. Por ejemplo:

class B {
  int i;
  void func();
  ...
};
class A {                  // clase anfitriona
  ...
  friend void B::func();   // declara B::func método invitado
};


§6.1  No
es posible incluir una definición en la declaración de invitado. Solo es posible utilizar friend con el prototipo de la función o clase.

class A {              // clase anfitriona
  ...
  friend class B {     // Error!!
  /* definición de B */
  };
};

§7  Resolver problemas de declaración:

Como se ha visto, la declaración de una clase o una función externa, como invitadas puede hacerse de la forma:

class B;                   // Declaración adelantada, para que
                           // el compilador sepa que B es una clase
class A {                  // Clase anfitriona
  friend B;                // Declara B clase invitada
  friend int foo();        // L.4: Declara foo función invitada
  ...
};
...
class B { /* ... */ };     // definición de la clase B
int foo() { /* ... */ };   // L.9: declaración de función foo


El trozo anterior compila correctamente. Sin embargo, si friend se refiere a una función-miembro, la forma anterior produciría un error. Por ejemplo:

class B;                   // declaración adelantada
class A {                  // Clase anfitriona
  friend int B::foo();     // L.3: declara invitado el método foo de B
  ...
};
...
class B {                  // definición de la clase B
  int x;
  int foo() { return x; }  // L.9: método (inline)
};


Este trozo de código produce sendos errores en los dos compiladores probados; en MS Visual C++ 6.0: L.3: use of undefined type 'B'; en Borland C++ 5.5: L.3: 'B::foo()' is not a member of 'B'.

Es fácil deducir que el error se produce porque los compiladores no son lo suficientemente "inteligentes" como para salvar la situación, ya que se está declarando invitado un método B::foo() que todavía no está definido (su definición está unas líneas más adelante, en L9). Observe que es exactamente el caso de la función foo() en la línea 4 del ejemplo anterior , donde sin embargo ambos compiladores aceptan sin rechistar que la definición de la función se efectúa unas líneas después, en L.9 [2].

Para salvar esta última situación, se recurre a definir la clase invitada antes que la anfitriona, con lo que no hay necesidad de recurrir a la declaración adelantada:

class B {                  // definición de la clase B
  int x;
  int foo() { return x; }  // método (inline)
};
...
class A {                  // definición posterior de clase anfitriona
  friend int B::foo();     // Ok: declara foo de B método invitado
  ...
};


Una última observación: si el método B::foo() no fuese "inline", se podría definir después de la clase A. Para los efectos que nos ocupan, es suficiente con el prototipo existente en la definición de B:

class B {                // definición de la clase B
  int x;
  int foo();             // método (prototipo)
};
...
class A {                // definición de clase anfitriona
  friend int B::foo();   // Ok: declara f2 de B método invitado
  ...
};
...
int B::foo() { return x; }  // definición del método foo de B


Observe que la única forma de declarar relaciones de "amistad" mutua entre dos clases, es declarar, toda la clase definida en segundo lugar, friend de la primera:

class B {                   // definición de clase anfitriona
  friend class A;           // Ok. declara A clase invitada
  int foo() { /* ... */ };  // método
  ...
};
...
class A {                   // definición de clase anfitriona
  friend int B::foo();      // Ok: declara f2 de B método invitado
  ...
};

§9  Observaciones

Respecto a este especificador es importante tener en cuenta los siguientes puntos.

  • La invitación puede ser a una subclase de la propia clase anfitriona.  Ver ejemplo ( 4.11.2b).

  • Es indiferente la zona (pública o privada) de la clase anfitriona donde se declare el invitado; este no se ve afectado por ningún especificador de acceso (public, private o protected).

  • Suponiendo func1 una función invitada de una clase A. Puesto que func1 no es miembro de A, no está en su ámbito. Por tanto, no puede ser referenciada como a.func1 o Aptr->func1 (donde a es un objeto de A y Aptr es un puntero-a-tipo A).

  • Una función "invitada" a una clase no tiene porqué pertenecer a ninguna otra clase (como el caso de func1 del ejemplo ), si bien en este caso no tiene puntero this ( 4.11.6).

  • Una función o clase pueden ser friend (invitada) en cualquier número de clases anfitrionas, pero una función solo puede ser miembro de una clase (o no pertenecer a ninguna [1]).

  • Una función no puede ser invitada (friend) y miembro de la clase anfitriona al mismo tiempo (no tendría sentido).

  • Si una clase es invitada de otra, todos sus métodos son invitados de la clase anfitriona aunque no se indiquen explícitamente; caso de las funciones setAc y getA del ejemplo (); ambas son funciones amigas de la clase A.

  • La cualidad de invitado no es heredable, es decir, la clase F es invitada de la clase A, pero no lo son las clases derivadas de F ( 4.11.2b).

  • La accesibilidad del modificador friend es asimétrica (en realidad los "friend" no son buenos amigos ;-) es decir: los miembros de la clase invitada tienen acceso a los de la clase anfitriona, pero no a la inversa (aunque dos clases pueden declararse recíprocamente invitadas, ver el ejemplo que sigue).

class A {
   ...
   friend class B;
};
class B {
  ...
   friend class A;
};

este trozo de código puede ser expresado también en la forma siguiente:

class B;        // L.1: declaración anticipada de B
class A {
   ...
   friend B;    // L.4:
};
class B {
  ...
   friend A;    // L.8:
};

en esta última forma sintáctica la declaración anticipada ( 4.11.4a) de B en L.1, sirve para que el compilador sepa que B es una clase al llegar a L.4, y no produzca un error: Friends must be functions or classes, que se produciría si no se incluyera.

  • La accesibilidad del modificador friend no es transitiva es decir:  Si A es invitada de B y B es invitada de C, esto no implica que A sea friend de C.

  • Una función virtual ( 4.11.8a) no puede ser friend de ninguna clase [3].

  • Un constructor ( 4.11.2d) no puede ser friend de ninguna otra clase [4].


§10  Es posible declarar friend a una función y definirla en el mismo punto de la declaración [5]. En este caso la función es declarada implícitamente inline, y pertenece al ámbito de la clase.

Considere cuidadosamente el siguiente ejemplo:


#include <iostream>
using namespace std; 

int y = 13;

class A {
  int x;
  public:
  static int y;
  friend void f1(const A o) {
    cout << "Miembro x = " << o.x << endl;
    cout << "y = " << y << endl;
  }
  friend void f2(const A o);
  A(int a = -1) {       // constructor
    x = a;
  }
};
int A::y = 31;

void f2(const A o) {
  cout << "Miembro x = " << o.x << endl;
  cout << "y = " << y << endl;
}
void main() {        // ===============
  A a;               // M1:
  f1(a);             // M2:
  f2(a);             // M3:
}

Salida:

Miembro x = -1
y = 31
Miembro x = -1
y = 13

Comentario:

La clase define dos funcinoes friend, f1 y f2.  La primera se define en el punto de su declaración; la segunda se define fuera de la clase.

La primera observación importante es que, a pesar de lo que podría parecer, f1 no es un método de A; es una función del espacio global del fichero, igual que f2, como lo demuestra la invocación M2, que es responsable de las dos primeras salidas.

Ambas funciones tienen acceso a los miembros privados de A, como lo demuestran las salidas 1 y 3. Sin embargo, las salidas 2 y 4 presentan resultados distintos. La primera, producida por f1, se refiere al miembro A::y. La segunda, producida por f2, se refiere al miembro global ::y.

La razón es que el cuerpo de f1 está en el ámbito de A [6],  al encontrar el compilador el identificador y en f1, el mecanismo de "Name-lookup" conduce al ámbito de A, en el que se produce una concordancia con el miembro estático A::y.

Por contra, el mismo proceso realizado en f2, que está en el espacio global del fichero, conduce al ámbito global, donde la concordancia se produce con el miembro global ::y.

  Tema relacionado
  • El especificador friend con funciones genéricas (plantillas) ( 4.12.2a

  Inicio.


[1]  Recuerde que C++ es un lenguaje híbrido, que incluye capacidades de POO y de programación tradicional.

[2]  Este tipo de "particularidades", de los actuales compiladores C++ (recuerde que tanto MS Visual C++ como Borland C++ son productos punteros), hacen que la programación C++ tenga tan mala prensa, y que en especial para el neófito pueda llegar a ser desesperante. Un caso como el presentado, sobre todo si se ha compilado antes sin problemas el ejemplo precedente, puede hacernos perder un buen rato sin entender en principio que C...~#%$!!@ está mal. En el peor de los casos pueden mandarnos a limpiar cochineras a Carolina del Norte ( 1 :-)

[3]  Stroustrup TC++PL,  & 8.5.4

[4]  Stroustrup TC++PL,  & 8.5.5

[5]  "members of the Core Language group discovered that a friend declaration of a member function can also define that member. Actually, they discovered that even though they didn't know what it meant, nothing in the ARM precluded such a definition". Dan Saks "Looking Up Names" C/C++ Users Journal  August 1993.

Nota: ARM es el acrónimo por el que se conoce un texto clásico sobre C++: Margaret A. Ellis and Bjarne Stroustrup. The Annotated C++ Reference Manual (Addison-Wesley, 1990) ACRM.

[6]  Observe que es un concepto difícil de asimilar si no se está acostumbrado a las sutilezas gramaticales: la función está en el ámbito de la clase, pero no es un miembro de ella.