17 Mar, 2013

Episode Three: Friends with Benefits

In C++, a friend of a class is a function or class that is given permission to use the private and protected member names from the class. In these lands, a friend is someone who can touch one's private parts...

Friendship

A class specifies its friends, if any, by way of friend declarations. A friend declaration punches a hole in the access control system, which is sometimes seen as breaking encapsulation. If used properly, friendship can enhance encapsulation by giving access to only a few number of tightly coupled classes or functions. Thus, friendship avoids the need for artificial interfaces which should not be part of the interface yet are available for the world to use —it punches a smaller hole than lessening access restrictions—. However, excessive friendship can often be an indication of a bad design with high coupling and little encapsulation. As every other tool, it should not be ignored nor abused.

A friend declaration is intrusive, as it can only be done from within the body of the class granting friendship. A friend declaration is not a member of the class, so the access specifier of the member block in which is introduced is irrelevant:

[11.3/9] [...] The meaning of the friend declaration is the same whether the friend declaration appears in the private, protected or public (9.2) portion of the class member-specification.

An example

Lets assume we are modeling an elevator and a repairman —inspired by this article—. We will ignore the transition between floors; the elevator will be at one of the floors, and it may be operational or broken. We can't leave the implementation details accessible or we may end in a state that violates the invariant —i.e., being at a floor that does not exist—, so the member objects where the elevator state are stored will be private. The repairman is no regular people, and needs access to the elevator internals to fix it were it to be out of order.

class repairman;

class elevator {
  friend repairman;
public:
  int floor() const { return _floor; }
  void goto_floor( int floor ){ /*check we have a valid floor*/_floor = floor; }
  bool is_operational() const { return !_broken; }
private:
  int _floor = 0;
  bool _broken = false;
};

class repairman {
public:
  void fix( elevator& e ) const { e._broken = false; } // that was easy!
};

In this particular design, friendship does not break encapsulation. Someone has to fix the elevator, and we only let the repairman do it. The repairman class is now coupled to the elevator class; if the elevator implementation changes then the repairman will have to learn how to fix the new mechanism.

To avoid coupling, we can place the functionality to fix the elevator within the elevator class itself:

class elevator {
  friend repairman;
  :::
private:
  void fix(){ _broken = false; }  
};

class repairman {
public:
  void fix( elevator& e ) const { e.fix(); } // that was even easier!
};

The repairman still has access to all the implementation details of an elevator. As long as it does not make use of said access and restricts itself to calling the fix member function, then it won't have to learn anything new when the implementation of elevators changes.

A note on name lookup

Instead of forward declaring the repairman class, we could have just introduced its name at the friend declaration itself —friend class repairman;—. For our particular purposes, both of them get the job done, however they are not quite the same. When a name is introduced at a friend declaration, it is not found by either qualified nor unqualified lookup until a matching declaration is provided. This applies to both classes and functions. When a friend function is defined within the befriending class, it can only be found by argument-dependent name lookup. Thus, it is possible to define friend functions that can never be called:

struct X
{
  friend void f( X& x ){} // only argument-dependent name lookup will find this name
  friend void g(){} // no lookup will find this name, function cannot be called
};

Algebra of Friendship

Friendship is reflexive

Control access is done on types rather than instances, so an instance of a class has access to private and protected member names of every other instance of the same type. Not all languages follow this rule —Eiffel and Ruby being examples of languages that don't—.

Friendship is not symmetric

Friendship is a directional relation. It is only mutual when both classes explicitly declare the other as a friend.

Friendship is not inherited nor transitive

Access is only given to names within the context of the befriending class, and only to the befriended class.

[11.3/10] Friendship is neither inherited nor transitive. [ Example:

class A {
    friend class B;
    int a;
  };
  
  class B {
    friend class C;
  };    
  
  class C {
    void f(A* p) {
      p->a++; // error: C is not a friend of A despite being a friend of a friend
    }
  };
  
  class D : public B {
    void f(A* p) {
      p->a++; // error: D is not a friend of A despite being derived from a friend
    }
  };

—end example ]

A note on nested classes

A nested class does not grant access to an enclosing class, if such access is intended then it has to be introduced by the use of a friend declaration at the nested class. Whether friendship is required for a nested class to access members of an enclosing class, on the other hand, is something that changed from C++03 to C++11.

C++03 [11.8/1] The members of a nested class have no special access to members of an enclosing class, nor to classes or functions that have granted friendship to an enclosing class.

C++11 [11.7/1] A nested class is a member and as such has the same access rights as any other member. The members of an enclosing class have no special access to members of a nested class; the usual access rules (Clause 11) shall be obeyed.

[ Example:

class E {
    int x;
    class B { };
    
    class I {
      B b; // C++03 - error: E::B is private
           // C++11 - OK: E::I can access E::B
      int y;
      void f(E* p, int i) {
        p->x = i; // C++03 - error: E::x is private
                  // C++11 - OK: E::I can access E::x
      }
    };
    
    int g(I* p) {
      return p->y; // C++03 - error: I::y is private
                   // C++11 - error: I::y is private
    }
  };

—end example ]

Fine-grained access

Friendship in C++ is all or nothing. When a function or class is declared as a friend it gains access to all protected and private names. Some languages offer a form of restricted access, in which access is granted only to certain members.

How Eiffel does it

Unlike other languages, having notions of public, private and so on, it uses an exporting technology to more precisely control the scoping between client and supplier classes. For example, the {NONE} is similar to private in other languages.

feature {NONE}

Objects of a class type cannot access such private features of another instance of the same type, since access is enforced per-instance. The equivalent of C++'s private is accomplished by giving explicit access to the current class being defined.

feature {NAME_OF_CLASS_BEING_DEFINED}

Alternatively, the lack of a {x} export declaration implies {ANY} and is similar to the public scoping of other languages.

feature
feature {ANY}

Finally, scoping can be selectively and precisely controlled to any class in the Eiffel project universe, such as:

feature {DECIMAL, DCM_MA_DECIMAL_PARSER, DCM_MA_DECIMAL_HANDLER}

Here, the compiler will allow only the classes listed between the curly braces to access the features within the feature group (e.g. DECIMAL, DCM_MA_DECIMAL_PARSER, DCM_MA_DECIMAL_HANDLER).

C++ based approaches

Such behavior can —to some extent— be emulated in C++.

Piece-wise class definition

We can reduce the amount of access granted by reducing the number of members in the class. If we can split the implementation into several classes, we can reduce the scope of the friendship. This approach does what we intend to do, but it cripples the design in the process.

We could change our elevator example like this:

class elevator_floor {
public:
  int floor() const { return _floor; }
  void goto_floor( int floor ){ /*check we have a valid floor*/_floor = floor; }
private:
  int _floor = 0;
};
class elevator_broken {
  friend repairman;
public:
  bool is_operational() const { return !_broken; }
protected:
  void fix(){ _broken = false; }
private:
  bool _broken = false;
};
class elevator : public elevator_floor, public elevator_broken {};

The repairman can access the fix member function. The repairman can't access _floor but it can access _broken —i.e., it cannot make the elevator think it is in a different floor than the one is at, but it can still break it—.

Proxy accessors classes

We can create interfaces to reduce the access scope of friendship by means of proxy classes. Instead of granting friendship to a target class, we grant it to a proxy class that will only expose a limited interface. That proxy class in turn will grant friendship to the target class. This approach only changes the interface slightly, as privileged access has to be done via static functions in the proxy class.

We could change our elevator example like this:

class elevator {
  friend elevator_fix_access;
  :::
private:
  void fix(){ _broken = false; }
};
class elevator_fix_access {
  friend repairman;
private:
  static void fix( elevator& e ){ e.fix(); }
};

class repairman {
public:
  void fix( elevator& e ) const { elevator_fix_access::fix( e ); }
};

The repairman is the only one that can obtain access to the fix member function, via the elevator_fix_access proxy class. The proxy class can access all of the elevator internals, but the repairman can't access any of them —it can only interact with it via the proxy class—.

High-order friendship

We can turn friendship into objects by means of access keys. A template access key can be defined as:

template< typename T >
class access_key {
  friend T; // only T can construct keys
private:
  access_key() {} // default constructor is private
};

Adding an access key as an argument to a public function will prevent that function from being called in a context other than those in which an access key can be obtained. Only objects of type T can construct an access_key<T>, which grants them access to the function. Since access keys can be copied or passed by reference, an object of type T can also effectively pass friendship around. A disadvantage of this approach is that function signatures must be changed, which limits its usability —i.e., it cannot be used with most operators—.

We could change our elevator example like this:

class elevator {
  :::
public:
  void fix( access_key< repairman > ){ _broken = false; }
};

class repairman {
public:
  void fix( elevator& e ) const { e.fix( access_key< repairman >{} ); }
};

The repairman is the only one that can obtain access to the fix member function, although it can share said access to a trusted party. The repairman can't access any of the elevator internals —it can only interact with it via the extended interface (public + fix)—.

Summary

Friendship is a language tool to bypass access control rules. Friends are introduced by way of friend declarations; said declarations grants the friend access to private and protected members of the befriending class.

  • Friendship does not necessarily break encapsulation if used properly.
  • The access at the point a friend declaration is introduced is irrelevant.
  • Friendship is not symmetric, inherited nor transitive.
  • The scope of access granted by friendship can be reduced by (ab)using the type system.

References: