Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 16.  Keywords


16.3. owner

The three access control flavors are private, protected, and public. The semantics of private and public are pretty clear: private means that nothing outside the class (apart from any friends) can see or use the given members (variables or methods); public means that the members are accessible to any other code.

protected means that only things within the class (or friends) or derived classes can see or use the given members; it provides access to parts of a base class type to a derived type where the types are related in an inheritance relationship. Occasionally, it would be useful to allow access to otherwise inaccessible parts of an inner type to a separate outer type where the types are involved in a composition relationship. We see a perfect example of this when we look at how to provide properties in C++ in Chapter 35. Unfortunately C++ does not support this type of access control.

Imperfection: C++ does not provide access control between types involved in a composition relationship.


What we'd like to see is something like the following, where the new access specifier owner grants composing types access to the constructor of Inner:

Listing 16.1.


class Inner


{


owner:


  Inner(Resource *r)


    : m_r(r)


  {}


public:


  ~X();


  . . .


};





class Outer


{


public:


  Outer( . . . )


    : m_inner(GetResource(. . . ))


  {}


private:


  Inner m_inner;


};



If you are coding a specific type, which will be owned by one specific type, or a few specific types, the class can be coded granting the owning type(s) as friend(s). However, this is very specific and highly coupled.



class Inner


{


private:


  friend class Outer; // Coupling: brittle!


  Inner(Resource *r)


    : m_r(r)


  {}



Each time you need to allow Inner to be composable in a new type, its class definition will need to be amended, and therefore all code involving existing composable types will have to be recompiled. This is not a solution, merely an intensive way of making new problems for oneself. Clearly the only usable form of this technique is going to be a generic one, and there are two ways to achieve this.

The first is that one can define as protected the members that are to be owner accessible, and then derive private member classes within each composing class.

Listing 16.2.


class Inner


{


protected:


  Inner(Resource *r);


  . . .


};





class Outer


{


public:


  Outer( . . . )


    : m_inner(GetResource(. . . ))


  {}


private:


  class OuterInner


    : public Inner


  {


  public:


    OuterInner(Resource *r) // Forwarding constructor


      : Inner(r)


    {}


  };


  OuterInner m_inner;


};



This effectively grants owner access, but the downside is that there's still a finite amount of coding to be done for each case in tiresome constructor forwarding (see Chapter 23). Further, it only works to provide access for methods; it cannot grant direct access to fields.

The second technique is to use templates, and to define one of the parameterizing types as a friend of the template, as in:



// form #1


template <typename T>


class Thing


{


  friend T; // Allow T to see inside Thing<T>


private:


  int m_value;


};



This seems like an eminently reasonable thing to do, does it not? Alas, it is not legal C++. The standard states that "within a class template with a template type-parameter T, the declaration ["]friend class T;["] is ill-formed" (C++-98: 7.1.5.3(2)). However, we're imperfect practitioners, not language lawyers, so that does not worry us, as long as what we're doing is sensible, which it is in this case.

The form shown above, which I'll call form #1, works with the following compilers: Borland (5.51 and 5.6), Comeau (4.3.0.1), Digital Mars, GCC (2.95), Intel (6 and 7), Watcom (11 and 12), and Visual C++ (4.2–7.1). It does not work with CodeWarrior (7 and 8) or GCC (3.2). Note that Comeau (4.3.0.1) works with this form, and all others, when in its Win32 default configuration. In strict mode (—A) it does not work with any, reflecting the fact that the technique in all of its forms is not legal C++. However, from version 4.3.3 onward, Comeau contains a --friendT option, which Greg Comeau kindly added after only a modest amount of badgering. Now we can use Comeau in strict mode, but still have this most useful construct. (We see exactly how useful in Chapter 35.)

Since T is a class—we're granting it friendship, so it can't exactly be an int—maybe we should be mentioning that fact to the compiler, as in:



// form #2


template <typename T>


class Thing


{


  friend class T; // Allow T to see inside Thing<T>


private:


  int m_value;


};



This is form #2. CodeWarrior, Digital Mars, and Watcom support this form. So a bit of compiler discrimination between forms #1 and #2 would cover most bases, but we're still not satisfying GCC 3.2.

Being someone that avoids friendship like the plague,[2] I ran out of experience here and needed to consult with the kind people of the comp.lang.c++.moderated newsgroup, who suggested some alternatives. The suitable one I'll call form #3:

[2] I think that the friend keyword is way overused in the general development community. I'm not, of course, arguing that it should never be used. I think everything's got its place, even goto, but just that it's a very rare circumstance where friend is needed.



// form #3


template <typename T>


class Thing


{


  struct friend_maker


  {


    typedef T T2;


  };


  typedef typename friend_maker::T2   friend_type;


//  friend class friend_type;


  friend friend_type;


private:


  int m_value;


};



The complication here is we're back to the inconsistency seen in forms #1 and #2 between whether the class specifier should be used in the friend statement. GCC and Visual C++ both require that class is not used, whereas the other compilers require that it is used. But since GCC is the only one for which we (currently) require this third form, we'll omit the class specifier. Table 16.1 summarizes the support.

Table 16.1. Friendship support of various compilers

Compiler

Form #1

Form #2

Form #3

Borland C++ (5.51 & 5.6)

Yes

No

No

CodeWarrior (7 & 8)

No

Yes

Yes

Comeau (4.3.3)

with --friendT

with --friendT

with --friendT

Digital Mars (8.26 – )

Yes

Yes

Yes

GCC 2.95

Yes

No

No

GCC 3.2

No

No

Yes

Intel (6 & 7)

Yes

Yes

Yes

Visual C++ (4.2 – 7.1)

Yes

No

Yes

Watcom (11 & 12)

Yes

Yes

Yes


The language says it's illegal, so it's not surprising that there's variance in the compilers' "illegal" support. I really don't like macros as a rule, especially ones that generate code, but in this case it's probably necessary. Hence, we can define the macro DECLARE_TEMPLATE_PARAM_AS_FRIEND() as shown in Listing 16.3.

Listing 16.3.


#if defined(__BORLANDC__) || \


    defined(__COMO__) || \


    defined(__DMC__) || \


    (   defined(__GNUC__) && \


        __GNUC__ < 3) || \


    defined(__INTEL_COMPILER) || \


    defined(__WATCOMC__) || \


    defined(_MSC_VER)


# define    DECLARE_TEMPLATE_PARAM_AS_FRIEND(T)   friend T


#elif defined(__MWERKS__)


# define    DECLARE_TEMPLATE_PARAM_AS_FRIEND(T)   friend class T


#elif defined(__GNUC__) && \


      __GNUC__ >= 3


# define    DECLARE_TEMPLATE_PARAM_AS_FRIEND(T)   \


  struct friend_maker                             \


  {                                               \


    typedef T T2;                                 \


  };                                              \


  typedef typename friend_maker::T2 friend_type;  \


  friend friend_type


#endif /* compiler */



It is then used as follows:



// form #2


template <typename T>


class Thing


{


  DECLARE_TEMPLATE_PARAM_AS_FRIEND(T);


private:


  int m_value;


};



Note that I've defined the macro so that it needs a terminating semicolon in the code where it is used; you may choose to do it otherwise. As I've made clear in the example code, the template friend technique can provide access to fields as well as methods, but it's not strictly legal even though it is very widely implemented. It also grants all-or-nothing access, rather than the fine-grained control I outlined at the start of the section.

This is one of a very few instances in this book where I advocate stepping outside the standard. I believe it's okay in this case for two reasons. First, almost all our compilers (see Appendix A) support it at the moment, and it seems highly unlikely that there'll be a mass change to break a potentially large existing code base. Second, it's a sensible thing to want to do, since it only apes perfectly legal behavior of nontemplate classes. The fact that the Comeau compiler—arguably the most compliant compiler in the industry—has seen fit to add the --friendT option to their latest version (version 4.3.3) is a good indicator that this is not a foolish thing to want to do.

The inheritance technique is legal, but requires new code for each composing class. Also, it cannot directly access fields; accessor methods are required, which further increase the new code that needs to be written, with the concomitant increase in maintenance and testing.


      Previous section   Next section