Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 2.  Object Lifetime


2.2. Controlling Your Clients

One of the important and powerful features of C++ is its ability to enforce access control at compile time. By using the public, protected, and private [Stro1997] access specifier keywords, coupled with sparing use of the friend keyword, we can moderate the ways in which client code can use our types. These moderations can be extremely useful in a variety of ways, many of which we'll be using in the techniques described in this book.

2.2.1 Member Types

A powerful way to control the manipulation of your class instances is to declare members of the class const and/or of reference type. Because both constants and references (and const references) can only be initialized, and cannot be assigned to, using either in a class definition prevents the compiler from defining a copy assignment operator. More than that, however, it helps you, the original author, and any maintainers of your code by enforcing your original design decisions. If changes cannot be made to the class without violating these restrictions—which your compiler will help you out with—then that indicates that the design may need to change, and flags the importance of any changes that you may need to make to the class. It's classic hairshirt programming.

(Note that this is primarily a technique for enforcing design decisions within the class implementation, such decisions being "passed down" to future maintainers. It is not appropriate for users of your class to learn that it should not be used in copy-assignment from the compiler warning about const members. Rather they should glean this from your explicit use of copy assignment protection, as described below.)

2.2.2 Default Constructor

This gets hidden automatically if you define any other constructor, so you don't have to try too hard to hide it. This behavior makes sense if you're defining a class whose constructor takes parameters, then not having a default constructor (one that is not acquiring anything) is reasonable. Consider a class that acts to scope a resource (see Chapter 6): there'd be no sense having a default constructor. What would be released?

Hiding the default constructor in a class that has no other constructor defined prevents any instances of that class being created (apart from friends or static methods of the class itself), though it does not prevent their being destroyed.

2.2.3 Copy Constructor

Whether or not you define a default, or any other, constructor, the compiler will always synthesize a copy constructor if you don't explicitly provide one. For certain kinds of classes— for example, those that contain pointers to allocated resources—the default member-wise copy of a compiler provided copy constructor can result in two class instances thinking that they own the same resource. This does not end happily.

If you do not want a copy constructor, you should render it inaccessible using the idiomatic form [Stro1997]:



class Abc


{


   . . .


// Not to be implemented


private:


  Abc(Abc const &);


};



Only when your type is a simple value type (see Chapter 4), and has no ownership over any resources (see Chapter 3), is it advisable to allow the compiler to supply you with a default implementation.

2.2.4 Copy Assignment

As is the case with copy constructors, the compiler will generate a copy assignment operator for your class if you do not define one. Once again, you should only allow this in cases where simple value types are concerned.

If you have const/reference members, the compiler will not be able to generate a default version, but you should not rely on this to proscribe copy assignment for two reasons. First, you'll get lots of irritating compiler warnings, as it attempts to warn you of your presumed oversight.

Second, the use of const members is a mechanism for enforcing design that may reflect a lack of copyability, but it more often reflects overall immutability. If you change the immutability assumption, you may still wish to proscribe copy assignment. Hence it is better to explicitly declare it, using the idiomatic form [Stro1997]:



class Abc


{


   . . .


// Not to be implemented


private:


  Abc &operator =(Abc const &);


};



More often than not, proscribing copy assignment and copy construction go together. If you need to have one without the other, using method hiding you can achieve this straightforwardly.

2.2.5 new and delete

These operators are used to create an element on the heap, and to destroy it. By restricting access to them it means that instances of the class must be created on the frame (globals and/or stack variables). The canonical form of hiding operators new and delete is similar to that for hiding copy-constructor and copy-assignment operator. If you wished to hide the operators in a given class and any derived class (excepting those that redefined them, of course), then you would normally not provide an implementation, as in:



class Shy


{


  . . .


// Not to be implemented


private:


  void *operator new(size_t);


  void operator delete(void *);


};



In addition to controlling access, we can also usefully choose to implement our own versions of these operators, on a per-class basis [Meye1998, Stro1997] and also on a link-unit basis (see sections 9.5 and 32.3).

However, there are limitations to the use of access control with these operators, because they can be overridden in any derived class. Even if you make them private in a base class, there is nothing to stop derived classes from defining their own public ones. So restricting access of new and delete is really just a documentation tactic.

Notwithstanding this, one useful scenario can be to declare (and define) them as protected in a base class, thereby prescribing a common allocation scheme, and allowing any derived classes that are to be heap based to redefine their own public versions in terms of the protected ones they've inherited.

2.2.6 Virtual Delete

There's another interesting feature of delete, which is observed when you have a virtual destructor in your class. The standard (C++-98: 12.4;11) states that "non-placement operator delete shall be looked up in the scope of the destructor's class." Although providing a virtual destructor and hiding operator delete is a pretty perverse thing to do, if you do so you should be aware that you will have to provide a stubbed implementation to avoid linker errors.

2.2.7 explicit

The keyword explicit is used to denote that the compiler may not use the constructor of the class in an implicit conversion. For example, a function f() may take an argument of type String, and can resolve a call to f("Literal C-string") by implicitly converting the literal string to String if String has a constructor such as



class String


{


public:


  String(char const *);


  . . .



A missing explicit facilitates compilation of conversions that may not be desired and can be costly. Applying the explicit keyword to the constructor instructs the compiler to reject the implicit conversion.



public:


  explicit String(char const *);



This is a widely documented [Dewh2003, Stro1997], and well-understood concept. Use of the explicit keyword on such so-called conversion constructors is recommended except where the class author specifically wants to facilitate the implicit conversion.

2.2.8 Destructor

By hiding the destructor we forcibly prevent the use of frame/global variables, and we also prevent delete being applied to an instance to which we have a pointer. This can be of use when we have access to an object belonging to something else that we can use but not destroy. It is especially useful in preventing misuse of reference-counted pointers.

Another thing worth pointing out is that the destructor is the preferred place to locate in-method constraints (see section 1.2) for template classes. Consider the template shown in Listing 2.1.

Listing 2.1.


template <typename T>


class SomeT


{


public:


  SomeT(char const *)


  {


    // Only constrained if this ctor is instantiated


    constraint_must_be_pointer_type(T);


  }


  SomeT(double )


  {


    // Only constrained if this ctor is instantiated


    constraint_must_be_pointer_type(T);


  }


  . . .



The constraint in any given constructor is only tested if that constructor is instantiated, and C++ only instantiates those template members that are needed. This is a good thing, as it allows for some seriously useful techniques (see section 33.2).

To ensure the constraint is applied, you would need to place the constraint in all constructors. However, there is only one destructor, so you can save yourself the typing and the maintenance headache by placing it there.



  . . .


  ~SomeT()


  {


    // Always constrained if instance of class is created


    constraint_must_be_pointer_type(T);


  }


};



Naturally, this also does not work if you don't actually create any instances of the type, and only call its static members. But in almost all cases you have your bases covered by exercising constraints in the destructor.

2.2.9 friend

Injudicious use of the friend keyword can serve to undo all the access control we may apply. It has its proponents, from whom I might be inviting powerful disagreement, but I think its use should be limited only to classes that share the same header file. Since it is a matter of good design [Dewh2003, Lako1996, Stro1997] that classes and sets of functions should exist in their own header files, only those that are intimately interdependent—such as a sequence class and its iterator class, or a value type and its free-function operators—should share the same header file. So give friends a wide berth, they'll only cause you trouble in the long run.

If you follow the practice of defining free-functions, as much as is possible, in terms of class methods, the need for friendship can be dramatically reduced. A good example of this is defining the operator +( X const &, X const) free function in terms of X::operator +=(X const &). We'll look at this in detail in Chapter 25.

In reviewing this section, I decided to bite the bullet and quantify my own (mis)use of the friend keyword. There are 41 uses of the keyword in the STLSoft libraries at the time of writing. Of these, 29 are sequence/iterator/value-type relationships and 8 involve the subscript subtypes of multidimensional array classes (see section 33.2.4). All of the other four involve same-file classes, sometimes with nested classes and their outer class. I was surprised to have so many instances of the keyword, but at least I haven't been transgressing my own advice.


      Previous section   Next section