Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Friends

What if you want to explicitly grant access to a function that isn’t a member of the current structure? This is accomplished by declaring that function a friend inside the structure declaration. It’s important that the friend declaration occurs inside the structure declaration because you (and the compiler) must be able to read the structure declaration and see every rule about the size and behavior of that data type. And a very important rule in any relationship is “who can access my private implementation?”

The class controls which code has access to its members. There’s no magic way to “break in” from the outside if you aren’t a friend; you can’t declare a new class and say “hi, I’m a friend of Bob!” and expect to see the private and protected members of Bob.

You can declare a global function as a friend, and you can also declare a member function of another structure, or even an entire structure, as a friend. Here’s an example :

//: 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);
} ///:~

struct Y has a member function f( ) that will modify an object of type X. This is a bit of a conundrum because the C++ compiler requires you to declare everything before you can refer to it, so struct Y must be declared before its member Y::f(X*) can be declared as a friend in struct X . But for Y::f(X*) to be declared, struct X must be declared first!

Here’s the solution. Notice that Y::f(X*) takes the address of an X object. This is critical because the compiler always knows how to pass an address, which is of a fixed size regardless of the object being passed, even if it doesn’t have full information about the size of the type. If you try to pass the whole object, however, the compiler must see the entire structure definition of X, to know the size and how to pass it, before it allows you to declare a function such as Y::g(X).

By passing the address of an X, the compiler allows you to make an incomplete type specification of X prior to declaring Y::f(X*). This is accomplished in the declaration

struct X;

The declaration simply tells the compiler there’s a struct by that name, so it’s OK to refer to it as long as you don’t require any more knowledge than the name.

Now, in struct X , the function Y::f(X*) can be declared as a friend with no problem. If you tried to declare it before the compiler had seen the full specification for Y, it would have given you an error. This is a safety feature to ensure consistency and eliminate bugs.

Notice the two other friend functions. The first declares an ordinary global function g( ) as a friend. But g( ) has not been previously declared at the global scope! It turns out that friend can be used this way to simultaneously declare the function and give it friend status. This extends to entire structures:

friend struct Z;

is an incomplete type specification for Z, and it gives the entire structure friend status.

Nested friends

Making a structure nested doesn’t automatically give it access to private members. To accomplish this you must follow a particular form: first define the nested structure, then declare it as a friend using full scoping. The structure definition must be separate from the friend declaration, otherwise it would be seen by the compiler as a nonmember. Here’s an example:

//: 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 {
  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);
  };
  friend Holder::Pointer;
};

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

void Holder::Pointer::initialize(Holder* h) {
  h = h;
  p = h->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();
  }
} ///:~

The struct Holder contains an array of ints and the Pointer allows you to access them. Because Pointer is strongly associated with Holder, it’s sensible to make it a member structure of Holder. Once Pointer is defined, it is granted access to the private members of Holder by saying:

friend Holder::Pointer;

Notice that the struct keyword is not necessary because the compiler already knows what Pointer is.

Because Pointer is a separate class from Holder, you can make more than one of them in main( ) and use them to select different parts of the array. Because Pointer is a class instead of a raw C pointer, you can guarantee that it will always safely point inside the Holder.

The Standard C library function memset( ) (in <cstring>) is used for convenience in the above program. It sets all memory starting at a particular address (the first argument) to a particular value (the second argument) for n bytes past the starting address ( n is the third argument). Of course, you could have simply used a loop to iterate through all the memory, but memset( ) is available, well-tested (so it’s less likely you’ll introduce an error) and probably more efficient than if you coded it by hand.

Is it pure?

The class definition gives you an audit trail, so you can see from looking at the class which functions have permission to modify the private parts of the class. If a function is a friend, it means that it isn’t a member, but you want to give permission to modify private data anyway, and it must be listed in the class definition so everyone can see that it’s one of the privileged functions.

C++ is a hybrid object-oriented language, not a pure one, and friend was added to get around practical problems that crop up. It’s fine to point out that this makes the language less “pure,” because C++ is designed to be pragmatic, not to aspire to an abstract ideal.

Contents | Prev | Next


Contact: webmaster@codeguru.com
CodeGuru - the website for developers.
[an error occurred while processing this directive]