C++ const Correctness

by

Even thorough this article is titled "C++ const Correctness", we're not going to talk about const yet. Instead, we're going to start by talking about functions and their parameters. Consider the following class and function:


  class Invoice {
   public:
    //...
   protected:
    std::vector<Item> items;
  };
  void f( Invoice i );

The consequence of calling f() includes the cost of making a copy of the Invoice object. You suffer this consequence because f() is declared to take its parameter by value. If the Invoice class is anything other than a trivial class, this can be an expensive and slow operation. In this case, the std::vector<Item> member variable could potentially have hundreds of items or more, all of which need to be copied when Invoice objects are passed by value. Further, passing objects by value might result in object "slicing", wherein the compiler generates code only to copy the Invoice object. If Invoice happened to be a polymorphic class, all the information in derived classes would be lost. For example:


  class Invoice {
   public:
    virtual ~Invoice();
    virtual int sum();
    //...
   protected:
    std::vector<Item> items;
  };
  class CustomerInvoice : public Invoice {
   public:
    virtual int sum();
    // ...
   protected:
    std::vector<CustomerItem> custItems;
  };

  CustomerInvoice c;
  f( c );              // oops: sliced

In this example, the CustomerInvoice is copied by value. Because f() is declared to take an Invoice, the compiler slices off the CustomerInvoice parts of the object, leaving f() with potentially misleading information. The Invoice::sum() member function is not called polymorphically within f()--this is a design error. In general, copying user-defined types by value should be avoided because of these problems.

You can solve this problem, however, by rewriting f() like this: void f( Invoice* i );. Here, f() is declared to take a pointer to an Invoice. This avoids copying Invoices by value--only the value of the pointer is copied--and also avoids the "slicing" problem. However, passing parameters by pointer shifts an additional burden onto f(): the function needs to protect itself from null pointers. It is much better to declare f() like this, void f( Invoice& i );. Here, Invoice is passed by reference. This also speeds things up tremendously, because only the address of the Invoice object is passed instead of a copy of the entire Invoice object. You never can create an invalid reference, so f() does not need to worry about invalid pointers. Slicing also is avoided, and f() now is able to exploit safely the polymorphic nature of Invoice. For example, f() may be defined as follows:


  void f( Invoice& i ) {
    int s = i.sum(); // calls correct member function
    //...  
  }

Even though passing by reference gives a huge speed boost compared to passing by value, you shouldn't be satisfied yet. The function f() suffers from another design error. Once the function f() is called, it has access to all the public member functions of Invoice, allowing it to modify the value of the Invoice object which was passed to it. This often is less than desirable. For example, f() may be defined like this:


  void f( Invoice& i ) {
    int s = i.sum();       
    i.giveDiscount( s/2 );  // oops: modifies the invoice
    //...  
  }

The entire public interface of Invoice is available, so f() is able to make some potentially unwanted modifications to the Invoice object. It would be better to get the speed benefits of passing user defined objects by reference and also limit the interfaces of those objects so that their internal state cannot be altered. More often than not, functions want to read the values of their parameters, not alter them--and especially not alter parameters passed by reference, as those values typically belong to the calling object or function that may not be expecting such underhandedness. And here's where const comes in. The function f() can be declared to take a constant value as its parameter. For example:


  void f( const Invoice& i ) { // constant parameter
    int s = i.sum();           // error: 'i' is const
    i.giveDiscount( s/2 );     // error: 'i' is const
    //...  
  }

In C++, a constant is declared using the const keyword. Objects declared const have a modified type whose value cannot be changed; their values become read-only. Here is the best of both worlds: efficiency through passing arguments by reference and better encapsulation through const, which exposes only a limited const interface of the object. Indeed, functions or member functions that typically read only the values of their parameters can enforce this usage directly by declaring their parameters const. For these reasons, passing objects by const reference is preferred. However, this means you must implement an appropriate const interface for your classes from the start; in other words, your class designs should be "const correct". More on that later. First, now that you can see an immediate practical benefit to declaring constant values, we should look at const in more depth.

About const

Objects declared const have a modified type whose value cannot be changed; they are read-only. For built-in types declared as const, this means that once initialized the compiler refuses to alter their values:


  void inc( int& x ) { x +=1; } 
  const int i = 1;           // initialize constant value
  i += 5;                    // error: 'i' is const
  inc( i );                  // error: 'i' is const

Usually, the const modifier comes immediately before the object it modifies. For example:


  const int ZERO = 0;
  const std::string msg( "Hello, World" );

When applied to pointers, special attention should be paid to the placement of const. A pointer is a variable itself, so the placement of const often determines whether it applies directly to the pointer, to the object pointed to or to both. For example:


  const std::string * str1 = &someString;
  std::string const * str2 = &someString;
  str2 = &anotherString;

Above, str1 is a pointer to a constant string. The const modifies the type std::string. Because there is no const * construct in C++, str2 also is a pointer to a constant string. Both str1 and str2 point to constant strings; therefore, it is impossible to alter the value of the strings they point to. However, it is possible to alter the pointers themselves, and indeed the str2 pointer is changed so that it points to another std::string. Deciphering these declarations can be difficult unless you read them backwards, from right to left. For example, the declaration of str1 can be read from right to left as "str1 is a pointer to a std::string which is const". And str2 can be read as "str2 is a pointer to a constant std::string". Although they are written differently, these declarations describe pointers of a similar type.


  std::string * const str3 = &someString;
  const std::string * const str4 = &someString;

The str3 variable demonstrates a pointer that is constant itself; the pointer value cannot be modified, although the argument pointed to can. It can be read as "str3 is a constant pointer to a std::string". Finally, str4 is the most extreme example. It can be read as "str4 is a constant pointer to a std::string which is const"; neither the pointer nor the string can be modified.

Const Member Functions

The const modifier becomes a part of the object type and also can be applied to user-defined types:


  class Invoice {    
   public:
   void giveDiscount( int d ) {
    //...
   }
  };
  const Invoice i;       // a constant user-defined object
  i.giveDiscount();      // error: 'i' is const

Because Invoice::giveDiscount() has the potential to alter the internal state of the object, the compiler refuses to let you invoke this member function on i, which is declared as const. However, individual member functions themselves can be declared const, which tells the compiler that those member functions do not alter the internal state of the object and therefore are safe to invoke on const objects. For example:


  class Invoice {                    
   public:
    int sum() const { // a constant member function
     //...
    }    
  };
  const Invoice i;    // declares a constant user-defined object
  int s = i.sum();    // ok: Invoice::sum() is const

C++ directly supports const as a language feature, so the compiler provides free compile-time checking of const objects. With properly designed interfaces that utilize const member functions, you are free to declare and use constant user-defined objects--only the const interface of those objects will be available. Designing interfaces that are const correct essentially boils down to declaring const member functions wherever possible. And these const member functions should be included early on in the design of your classes. Trying to retrofit them later often leads to a cascading effect throughout the applications that use your classes. It is much better to use const early and often in your designs.

When designing const member functions, you may encounter a situation in which altering the internal state of the class is necessary. For example:


  class CustomerInvoice : public Invoice {   
   public:
    int sum() const {
     // calculate sum, and save for later
     sumCache = items.size() + custItems.size();  // error
     return sumCache;
    }    
    //...
   protected:
    //...
    int sumCache;
  };

In the above example, the value of sumCache cannot be altered, because CustomerInvoice::sum() is declared to be const. However, we really do want to cache the value for later use; so how can this be done? The answer is "with the mutable keyword", which is the converse of const when applied to member variables. For example, CustomInvoice can be corrected as follows:


  class CustomerInvoice : public Invoice {   
   public:
    int sum() const {
     // calculate sum, and save for later
     sumCache = items.size() + custItems.size();  // ok
     return sumCache;
    }    
    //...
   protected:
    //...
    mutable int sumCache;         // mutable member variable
  };

Now, the compiler allows sumCache to be modified within the CustomerInvoice::sum() const member function because sumCache was declared to be mutable. The mutable keyword instructs the compiler to accept changes to the variable declared with it, even in const member functions.

Conclusion

As a general principle of class design, encapsulation is good. It reduces the complexity of your code and reduces the number of possible interactions between classes. Using const member functions increases encapsulation by restricting the ways in which an object can be used in certain circumstances, particularly when objects are passed by constant reference to functions or member functions. The const modifier is evaluated at compile-time, so it costs nothing at run-time.

You now have the tools required to make your classes const correct. A proper const interface allows the compiler to do a lot of type-checking work for you in situations where constant objects are used. Without a const interface, the speed and encapsulation benefits of constant user-defined types is lost. A class designed without const member functions hamstrings those who would use the class, as there otherwise would be no way to use the class when objects of its type are passed by const references. As shown above, users of your classes have good reason to manipulate them in this way. Being const correct then comes full-circle, as only the const interface to your class is available in these situations.

Many programming practices and conventions are available that can improve code in all types of ways. The notion of const, though, is supported directly by the language and provides a free compile-time mechanism to better encapsulate the concepts within your programs as classes. The most fundamental unit of design in C++ is the class. Concise, simple and const correct class interfaces improve portability, add flexibility and provide a foundation for extending your design to previously unforeseen purposes.

The public interfaces of your classes are the ones used by developers, including yourself. Interfaces that exploit the value of const modifiers promote looser coupling between classes by further limiting the public interface. Looser coupling provides a better foundation for expanding and re-tooling your classes for future situations. Designing for future changes is essential, and const correctness is an important principle to follow in future-proofing your class designs today.

Dave Berton is a professional programmer. He can be reached at mosey@freeshell.org.

Load Disqus comments

Firstwave Cloud