friends in c++

“… much as in real life, friends are often more trouble than they are worth.”
Scott Meyers
Effective C++

Friend classes in C++ are one of the oddest constructs. Despite their existence in the language, pretty much every discussion you encounter on the topic says it is best to avoid them. In fact, as other object-oriented languages have demonstrated, you don’t need them. Consider SmallTalk whose purist approach to object-orientation prevents objects of the same type from accessing each other’s private data. This, however, can result in rather awkward interfaces. As a simple example, if this were the case in C++, the following class could not be implemented as it is defined

class Audio {
private:

  AudioStream audioStream;

public:

  Audio(String audioFilePath);

  // Assume the obvious.
  //
  void moveTo(double timeInSeconds);

  void splice(Audio &source, double atTimeInSeconds) {

    moveTo(atTimeInSeconds);
    
    // Assume an AudioStream has an insert() operation defined.
    //
    audioStream.insert(source.audioStream);

 }
       
}

The problem would be that the operation splice() attempts to access the private attribute audioStream of the incoming source object. To solve this, the Audio class would need to define an
accessor to access the raw audio stream. This is probably not the intention of the designer since it doesn’t make sense to publicly make available such information.

For the most part, when explaining object-oriented programming, we make a big fuss about information hiding and designing for objects that expose only the bare essentials to the world, zealously protecting their personal memory. We try to use every conceivable metaphor and picture to illustrate the concept. Friends completely circumvent these ideas.

In my experience, information hiding can prove to be an odd concept to the novice programmer who is making the transition from the function-oriented world. Many students that I encounter have carried over habits of declaring global data. This isn’t inherently bad. The “systems” these students have developed are all contained in a single file – .c or .cpp depending on their point of entry. In the greater practice, these map to implementation files where, on occasion, the use of global constructs occur where they make sense, tucked away in a controlled environment from the rest of the world. Determining when global constructs are warranted requires experience. But for those that have an initial Mommy-Dearest reaction (“No. Global data. Ever!”), I would say striving to avoid any global data use is the right starting point. It sets the right tone.

The function-oriented world also utilizes complex data types whose structure is exposed to the public in the form of a struct or, to a lesser extent depending on how it is used, an array. Efforts can be made to hide the fields from a user via the use of pointers to hidden data constructs that can only be manipulated by functions in a provided library but doing so requires some advanced programming knowledge that is probably not going to be part of the typical introductory experience for students. Besides, once you start doing that, why not just change planes and use a class?

Though the above may seem a bit of a diversion, the point is that the use of the friend modifier enables those students default back to the monolithic, spaghetti-code approach the object-oriented education is trying to purge.

The go-to example for friend operations is in overloading the redirect operators, << and >> since there is no default definition for user-defined classes and we may want to be able to “cout” a string representation or “cin” a few values. For example,

class Scores {

private:

  vector<double> scores;

public:

  void addScore(double newScore);

  // Other operations omitted for brevity

  friend ostream& operator<<(ostream &out, Scores &source) {

    // whatever makes sense to push to an ostream

  }

  friend istream& operator>>(istream &in, Scores &source) {

    // whatever makes sense to accept from an istream

  }

}

This is a really odd choice as an example of friendship when discussing classes. While it seems like an object-oriented approach, it really isn’t. The befriending that is taken place is between the scores class and the global function. Further more, it’s more overloading than friendship – at best we could say it is equally an instance of overloading and friendship. We could accomplish the same thing by moving the declaration outside of the class. In doing so, the operations would no longer have explicit visibility to the private attribute scores.

Since there is a mechanism to add scores to the class (via addScore()), so the >> operator would be easily implemented, we would only need to provide a way to extract the scores to support the << operator. This would probably be a natural part of the class anyway. Keeping it simple, we would then have something to the effect of

friend ostream& operator<<(ostream &out, Scores &source) {
  
  // Whatever makes sense to push to an ostream

}

friend istream& operator>>(istream &in, Scores &source) {
  
  // Whatever makes sense to accept from an istream

}

class Scores {

public:

  void addScore(double newScore);

  vector<double> getScores();

}

So, why include it in the course? It seems to fall in the category of goto. Since it’s in the language we should explain it somewhere in our travels as it might be encountered and used wisely it probably has a purpose on the rare occasion. But in the end we say the same thing about both: pretend it doesn’t exist. The other reason I included it in a recent course is that it’s in the book. I feel I have an obligation to address the topic and provide some insight; I need to explain why the book says to avoid it and then proceeds to give an example of how to use it.

C++ is not unique on this issue. Java has a similar, if not worse, issue since classes in a package can access all but private members of their package peers. I’ll probably get around to commenting on this more completely later.

No comments:

Post a Comment