I l@ve RuBoard Previous Section Next Section

Solution

graphics/bulb_icon.gif

When I pose this kind of problem, I find that most people think the problem is on the easy side and address only the more-usual const issues. There are, however, subtleties that are worth knowing, hence this Item. See also the box "const and mutable Are Your Friends."



class Polygon 


{


public:


  Polygon() : area_(-1) {}


  void  AddPoint( const Point pt ) { InvalidateArea();


                                   points_.push_back(pt); }


  1. The Point object is passed by value, so there is little or no benefit to declaring it const. In fact, to the compiler, the function signature is the same whether you include this const in front of a value parameter or not. For example:

    
    
    int f( int ); 
    
    
    int f( const int );  // redeclares f(int)
    
    
                         // no overloading, there's only one function
    
    
    int g( int& );
    
    
    int g( const int& ); // not the same as g(int&)
    
    
                         // g is overloaded
    
    
    

    Guideline

    graphics/guideline_icon.gif

    Avoid const pass-by-value parameters in function declarations. Still make the parameter const in the same function's definition if it won't be modified.


    
    
    Point GetPoint( const int i )   { return points_[i]; } 
    
    
    
  2. Same comment about the parameter type. Normally, const pass-by-value is unuseful and misleading at best.

  3. This should be a const member function, since it doesn't change the state of the object.

  4. If you're not returning a builtin type, such as int or long, return-by-value should usually return a const value.[1] This assists client code by making sure the compiler emits an error if the caller tries to modify the temporary.

    [1] I want to point out that not everyone agrees with me on this point. In Lakos96 (pg. 618), the author argues against returning a const value, and points out that it is redundant for builtin types anyway (for example, returning "const int"), which he notes may interfere with template instantiation.

    For example, the user might write something like "poly.GetPoint(i) = Point(2,2);". If this were intended to work, GetPoint() should have used return-by-reference and not return-by-value in the first place. As we will see later, it makes sense for GetPoint() to be a const member function for another reason: GetPoint() should be usable on const Polygon objects in operator+().

    Guideline

    graphics/guideline_icon.gif

    When using return-by-value for non-builtin return types, prefer returning a const value.


    
    
    int   GetNumPoints()             { return points_.size(); } 
    
    
    
  5. Again, the function should be const.

    Incidentally, this is a good example of where not to write a const return-by-value. A return type of "const int" would be wrong in this case, because: (a) returning "const int" doesn't buy you anything, since the returned int is already an rvalue and so can't be modified; and (b) returning "const int" can interfere with template instantiation and is confusing, misleading, and probably fattening.

    
    
    double GetArea() 
    
    
    {
    
    
      if( area_ < 0 ) // if not yet calculated and cached
    
    
      {
    
    
        CalcArea();     // calculate now
    
    
      }
    
    
      return area_;
    
    
    }
    
    
    
  6. Even though this function modifies the object's internal state, it should be const. Why? Because this function does not modify the object's observable state. We are doing some caching here, but that's an internal implementation detail and the object is logically const even if it isn't physically const.

    Corollary: The member variable area_ should be declared mutable. If your compiler doesn't support mutable yet, kludge this with a const_cast of area_ and write a comment telling the next person to remove the const_cast once mutable is available梑ut do make the function const.

    
    
    private: 
    
    
      void InvalidateArea() { area_ = -1; }
    
    
    
  7. This function ought also to be const, if for no other reason than consistency. Granted, semantically it will be called only from non-const functions, since its purpose is to invalidate the cached area_ when the object's state changes. But consistency and clarity are important to good programming style, so make this function const too.

    
    
    void CalcArea() 
    
    
    {
    
    
      area_ = 0;
    
    
      vector<Point>:: iterator i;
    
    
      for( i = points_.begin(); i != points_.end(); ++i )
    
    
      {
    
    
        area_ += /* some work */;
    
    
      }
    
    
    }
    
    
    
  8. This member function definitely should be const, because it does not alter the externally visible state of the object. Note also that this function is called only from another const member function, namely GetArea().

  9. The iterator should not change the state of the points_ collection, so it ought to be a const_iterator.

    
    
       vector<Point> points_; 
    
    
       double        area_;
    
    
    };
    
    
    
    
    
    Polygon operator+( Polygon& lhs, Polygon& rhs )
    
    
    {
    
    
    
  10. The parameters should be passed by references to const, of course.

  11. Again, the return-by-value of an object should be const.

    
    
    Polygon ret = lhs; 
    
    
    int last = rhs.GetNumPoints();
    
    
    
  12. Because last should never change, say so by making its type "const int".

    
    
      for( int i = 0; i < last; ++i ) // concatenate 
    
    
      {
    
    
        ret.AddPoint( rhs.GetPoint(i) );
    
    
      }
    
    
      return ret;
    
    
    }
    
    
    

    (Once we make rhs a reference-to-const parameter, we see another reason why GetPoint() should be a const member function.)

    
    
    void f( const Polygon& poly ) 
    
    
    {
    
    
      const_cast<Polygon&>(poly).AddPoint( Point(0,0) );
    
    
    }
    
    
    

    Bonus: Using the result of the const_cast is undefined if the referenced object was declared as const梬hich it is in the case of f(cpoly) below. The parameter isn't really const, so don't declare it as const. It's tantamount to lying.

    
    
    void g( Polygon& const rPoly ) { rPoly.AddPoint( Point(1,1) ); } 
    
    
    
  13. This const is not only useless, because references are already const inasmuch as they cannot be reseated (changed to refer to a different object), but actually illegal.

    
    
    void h( Polygon* const pPoly ) { pPoly->AddPoint( Point(2,2) ); } 
    
    
    
  14. This const is equally useless, but for a different reason: Because we're passing the pointer by value, this makes as little sense as passing a parameter "const int" above. Think about it梚t's exactly the same thing, because this const merely promises that you won't modify the pointer value. That's not a very useful promise, because the caller can't possibly care or be affected by that decision.

    (If, in looking for the "bonus" part, you said something about this function being uncompilable梥orry, it's quite legal C++. You were probably thinking of putting the const to the left of the *, which would have made the function body illegal.)

    
    
    int main() 
    
    
    {
    
    
      Polygon poly;
    
    
      const Polygon cpoly;
    
    
      f(poly);
    
    
    

    This is fine.

    
    
    f(cpoly); 
    
    
    

    This causes undefined results when f() tries to cast away the const-ness of its parameter and then modify it. See above.

    
    
    g(poly); 
    
    
    

    This is fine.

    
    
      h(&poly); 
    
    
    }
    
    
    

    This is fine.

const and mutable Are Your Friends

Be const-correct: const and mutable are your friends.

I still sometimes come across programmers who think const isn't worth the trouble. "Aw, const is a pain to write everywhere," I've heard some complain. "If I use it in one place, I have to use it all the time. And anyway, other people skip it, and their programs work fine. Some of the libraries I use aren't const-correct either. Is const worth it?"

We could imagine a similar scene, this time at a rifle range: "Aw, this gun's safety is a pain to set all the time. And anyway, some other people don't use it either, and some of them haven't shot their own feet off…"

Safety-incorrect riflemen are not long for this world. Nor are const-incorrect programmers, carpenters who don't have time for hard hats, and electricians who don't have time to identify the live wire. There is no excuse for ignoring the safety mechanisms provided with a product, and there is no excuse for programmers too lazy to write const-correct code. (Perhaps not strangely, the same people who skip const tend to ignore compiler warnings, too. Illuminating, that.) In many development shops, skipping const and ignoring compiler warnings are "one-warning" offences, and rightly so.

Using const whenever possible makes for safer, cleaner code. It lets you document interfaces and invariants far more effectively than any mere /* I promise not to change this */ comment can accomplish. It's a powerful part of "design by contract." It helps the compiler to stop you from accidentally writing bad code. It can even help the compiler to generate tighter, faster, smaller code. That being the case, there's no reason why you shouldn't use it as much as possible, and there's every reason why you should.

Remember that the correct use of mutable is a key part of const-correctness. If your class contains a member that could change even for const objects and operations, make that member mutable. That way, you will be able to write your class's const member functions easily and correctly, and users of your class will be able to correctly create and use const and non-const objects of your class's type.

It's true that not all commercial libraries' interfaces are const-correct. That isn't an excuse for you to write const-incorrect code, though. (It is, however, one of the few good excuses to write const_cast.) Always make your own code const-correct, and when you call the broken library, you can use the appropriate const_casts wherever necessary梡referably with a detailed comment nearby grumbling about the library vendor's laziness and how you're looking for a replacement product.

If you still harbor any doubts about const, run (don't walk) and reread Item 21, "Use const whenever possible," in Meyers98. It's for good reason that the C language has had const since the pre-standardization days. Back in the early 1980s, a researcher named Stroustrup had designed this funny little language called C with Classes, which had a funny little keyword called readonly that was demonstrably useful for specifying cleaner interfaces and, therefore, writing safer code. The C folks at Bell Labs liked the idea, but preferred the name const. When the ANSI C committee got rolling a short time later, they adopted the same feature with the same name梐nd the rest, as they say, is history (Stroustrup94, page 90).

Don't shoot yourself (or your fellow programmers) in the foot. Always check your safety: Be const-correct. Write const whenever possible, and smile at the thought of cleaner, safer, tighter code.

That's it. Here is a revised version of the code that corrects the const issues noted above (but does not attempt to correct any other poor style).



class Polygon 


{


public:


  Polygon() : area_(-1) {}





  void        AddPoint( Point pt ) { InvalidateArea();


                                     points_.push_back(pt); }





  const Point GetPoint( int i ) const  { return points_[i]; }





  int         GetNumPoints() const { return points_.size(); }





  double GetArea() const


  {


    if( area_ < 0 ) // if not yet calculated and cached


      CalcArea();     // calculate now


    return area_;


  }





  private:


    void InvalidateArea() const { area_ = -1; }





    void CalcArea() const


    {


      area_ = 0;


      vector<Point>::const_iterator i;


      for( i = points_.begin(); i != points_.end(); ++i )


        area_ += /* some work */;


    }





    vector<Point>  points_;


    mutable double area_;


  };





  const Polygon operator+( const Polygon& lhs,


                           const Polygon& rhs )


  {


    Polygon ret = lhs;


    const int last = rhs.GetNumPoints();


    for( int i = 0; i < last; ++i ) // concatenate


      ret.AddPoint( rhs.GetPoint(i) );


    return ret;


  }


  


  void f( Polygon& poly )


  {


    poly.AddPoint( Point(0,0) );


  }





  void g( Polygon& rPoly ) { rPoly.AddPoint( Point(1,1) ); }


  void h( Polygon* pPoly ) { pPoly->AddPoint( Point(2,2) ); }





  int main()


  {


    Polygon poly;


    f(poly);


    g(poly);


    h(&poly);


  }


    I l@ve RuBoard Previous Section Next Section