5.3. Amigos (friends)

¿Que pasa si explícitamente se quiere dar acceso a una función que no es miembro de la estructura? Esto se consigue declarando la función como friend dentro de la declaración de la estructura. Es importante que la declaración de una función friend se haga dentro de la declaración de la estructura pues usted (y el compilador) necesita ver la declaración de la estructura y todas las reglas sobre el tamaño y comportamiento de ese tipo de dato. Y una regla muy importante en toda relación es, «¿Quién puede acceder a mi parte privada?»

La clase controla que código tiene acceso a sus miembros. No hay ninguna manera mágica de «colarse» desde el exterior si no eres friend; no puedes declarar una nueva clase y decir, «Hola, soy friend de Bob» y esperar ver los miembros private y protected de Bob.

Puede declarar una función global como friend, también puede declarar un método de otra estructura, o incluso una estructura completa, como friend. Aquí hay un ejemplo:

//: C05:Friend.cpp
// Friend allows special access

// Declaration (incomplete type specification):
struct X;

struct Y {
  void f(X*);
};

struct X { // Definition
private:
  int i;
public:
  void initialize();
  friend void g(X*, int); // Global friend
  friend void Y::f(X*);  // Struct member friend
  friend struct Z; // Entire struct is a friend
  friend void h();
};

void X::initialize() { 
  i = 0; 
}

void g(X* x, int i) { 
  x->i = i; 
}

void Y::f(X* x) { 
  x->i = 47; 
}

struct Z {
private:
  int j;
public:
  void initialize();
  void g(X* x);
};

void Z::initialize() { 
  j = 99;
}

void Z::g(X* x) { 
  x->i += j; 
}

void h() {
  X x;
  x.i = 100; // Direct data manipulation
}

int main() {
  X x;
  Z z;
  z.g(&x);
} ///:~

Listado 5.3. C05/Friend.cpp


struct Y tiene un método f() que modifica un objeto de tipo X. Aquí hay un poco de lío pues en C++ el compilador necesita que usted declare todo antes de poder hacer referencia a ello, así struct Y debe estar declarado antes de que su método Y::f(X*) pueda ser declarado como friend en struct X. Pero para declarar Y::f(X*), struct X debe estar declarada antes!

Aquí vemos la solución. Dese cuenta de que Y::f(X*) toma como argumento la dirección de un objeto de tipo X. Esto es fundamental pues el compilador siempre sabe cómo pasar una dirección, que es de un tamaño fijo sin importar el tipo, aunque no tenga información del tamaño real. Si intenta pasar el objeto completo, el compilador necesita ver la definición completa de X, para saber el tamaño de lo que quiere pasar y cómo pasarlo, antes de que le permita declarar una función como Y::g(X).

Pasando la dirección de un X, el compilador le permite hacer una identificación de tipo incompleta de X antes de declarar Y::f(X*). Esto se consigue con la declaración:

struct X;

Esta declaración simplemente le dice al compilador que hay una estructura con ese nombre, así que es correcto referirse a ella siempre que sólo se necesite el nombre.

Ahora, en struct X, la función Y::f(X*) puede ser declarada como friend sin problemas. Si intenta declararla antes de que el compilador haya visto la especificación completa de Y, habría dado un error. Esto es una restricción para asegurar consistencia y eliminar errores.

Fíjese en las otras dos funciones friend. La primera declara una función global ordinaria g() como friend. Pero g() no ha sido declarada antes como global!. Se puede usar friend de esta forma para declarar la función y darle el estado de friend simultáneamente. Esto se extiende a estructuras completas:

friend struct Z;

es una especificación incompleta del tipo Z, y da a toda la estructura el estado de friend.

5.3.1. Amigas anidadas

Hacer una estructura anidada no le da acceso a los miembros privados. Para conseguir esto, se debe: primero, declarar (sin definir) la estructura anidada, después declararla como friend, y finalmente definir la estructura. La definición de la estructura debe estar separada de su declaración como friend, si no el compilador la vería como no miembro. Aquí hay un ejemplo:

//: C05:NestFriend.cpp
// Nested friends
#include <iostream>
#include <cstring> // memset()
using namespace std;
const int sz = 20;

struct Holder {
private:
  int a[sz];
public:
  void initialize();
  struct Pointer;
  friend struct Pointer;
  struct Pointer {
  private:
    Holder* h;
    int* p;
  public:
    void initialize(Holder* h);
    // Move around in the array:
    void next();
    void previous();
    void top();
    void end();
    // Access values:
    int read();
    void set(int i);
  };
};

void Holder::initialize() {
  memset(a, 0, sz * sizeof(int));
}

void Holder::Pointer::initialize(Holder* rv) {
  h = rv;
  p = rv->a;
}

void Holder::Pointer::next() {
  if(p < &(h->a[sz - 1])) p++;
}

void Holder::Pointer::previous() {
  if(p > &(h->a[0])) p--;
}

void Holder::Pointer::top() {
  p = &(h->a[0]);
}

void Holder::Pointer::end() {
  p = &(h->a[sz - 1]);
}

int Holder::Pointer::read() {
  return *p;
}

void Holder::Pointer::set(int i) {
  *p = i;
}

int main() {
  Holder h;
  Holder::Pointer hp, hp2;
  int i;

  h.initialize();
  hp.initialize(&h);
  hp2.initialize(&h);
  for(i = 0; i < sz; i++) {
    hp.set(i);
    hp.next();
  }
  hp.top();
  hp2.end();
  for(i = 0; i < sz; i++) {
    cout << "hp = " << hp.read()
         << ", hp2 = " << hp2.read() << endl;
    hp.next();
    hp2.previous();
  }
} ///:~

Listado 5.4. C05/NestFriend.cpp


Una vez que Pointer está declarado, se le da acceso a los miembros privados de Holder con la sentencia:

friend Pointer;

La estructura Holder contiene un array de enteros y Pointer le permite acceder a ellos. Como Pointer está fuertemente asociada con Holder, es comprensible que sea una estructura miembro de Holder. Pero como Pointer es una clase separada de Holder, puede crear más de una instancia en el main() y usarlas para seleccionar diferentes partes del array. Pointer es una estructura en vez de un puntero de C, así que puede garantizar que siempre apuntará dentro de Holder.

La función de la librería estándar de C memset() (en <cstring>) se usa en el programa por conveniencia. Hace que toda la memoria a partir de una determinada dirección (el primer argumento) se cargue con un valor particular (el segundo argumento) para n bytes a partir de la dirección donde se empezó (n es el tercer argumento). Por supuesto, se podría haber usado un bucle para hacer lo mismo, pero memset() está disponible, bien probada (así que es más factible que produzca menos errores), y probablemente es más eficiente.