Personal tools
You are here: Home Blogs Weiyi Documents Effecitve C++

Effecitve C++

Effective and More Effective C++

  • Item 21:  Use const whenever possible

  • Item 22:  Prefer pass-by-reference to pass-by-value.

  • Item 24:  Choose carefully between function overloading and parameter defaulting.

  • Item 25:  Avoid overloading on a pointer and a numerical type. 

 

 

 

 

 

Item 21:  Use const whenever possible

 

    char *p              = "Hello";          // non-const pointer,
    // non-const data5 const char *p = "Hello"; // non-const pointer, // const data char * const p = "Hello"; // const pointer, // non-const data const char * const p = "Hello"; // const pointer, // const data
When what's pointed to is constant, some programmers list const before the type name. Others list it after the type name but before the asterisk. As a result, the following functions take the same parameter type:

 

    class Widget { ... };
    void f1(const Widget *pw);      // f1 takes a pointer to a
                                    // constant Widget object
    void f2(Widget const *pw);      // so does f2

 

For example, consider the declaration of the operator* function for rational numbers that is introduced in Item 19:

 

    const Rational operator*(const Rational& lhs,
                             const Rational& rhs);

Many programmers squint when they first see this. Why should the result of operator* be a const object? Because if it weren't, clients would be able to commit atrocities like this:

    Rational a, b, c;
    ...
    (a * b) = c;      // assign to the product
                      // of a*b!

 

The purpose of const member functions, of course, is to specify which member functions may be invoked on const objects. Many people overlook the fact that member functions differing only in their constness can be overloaded, however, and this is an important feature of C++. Consider the String class once again:

 

 

    class String {
    public:
    ...  
    // operator[] for non-const objects
    char& operator[](int position)  { return data[position]; }  
    char& operator[](int position)  { return data[position]; }
    // operator[] for const objects
    const char& operator[](int position) const  { return data[position]; }
    
    private:
      char *data;
    };
    
    
    String s1 = "Hello";
    cout << s1[0];                  // calls non-const
                                    // String::operator[]
    const String s2 = "World";
    cout << s2[0];                  // calls const
                                    // String::operator[]
    

Let's take a brief time-out for philosophy. What exactly does it mean for a member function to be const? There are two prevailing notions: bitwise constness and conceptual constness.

The bitwise const camp believes that a member function is const if and only if it doesn't modify any of the object's data members (excluding those that are static), i.e., if it doesn't modify any of the bits inside the object. The nice thing about bitwise constness is that it's easy to detect violations: compilers just look for assignments to data members. In fact, bitwise constness is C++'s definition of constness, and a const member function isn't allowed to modify any of the data members of the object on which it is invoked. 

 

There is one other time when casting away constness may be both useful and safe. That's when you have a const object you want to pass to a function taking a non-const parameter, and you know the parameter won't be modified inside the function. The second condition is important, because it is always safe to cast away the constness of an object that will only be read — not written — even if that object was originally defined to be const.

 

 

For example, some libraries have been known to incorrectly declare the strlen function as

follows: 

    size_t strlen(char *s);
    

Certainly strlen isn't going to modify what s points to — at least not the strlen I grew up with. Because of this declaration, however, it would be invalid to call it on pointers of type const char *. To get around the problem, you can safely cast away the constness of such pointers when you pass them to strlen

    const char *klingonGreeting = "nuqneH";        // "nuqneH" is
                                                   // "Hello" in
                                                   // Klingon
    size_t length = strlen(const_cast<char*>(klingonGreeting));
    

Don't get cavalier about this, though. It is guaranteed to work only if the function being called, strlen in this case, doesn't try to modify what its parameter points to.

 

 

Item 22:  Prefer pass-by-reference to pass-by-value.

 

Now consider a simple function returnStudent that takes a Student argument (by value) and immediately returns it (also by value), plus a call to that

function: ¤ Item E22, P4

    Student returnStudent(Student s) { return s; }
    
    Student plato;                      // Plato studied under
                                        // Socrates
    
    returnStudent(plato);               // call returnStudent
    

What happens during the course of this innocuous-looking function call? 

The simple explanation is this: the Student copy constructor is called to initialize s with plato. Then the Student copy constructor is called again to initialize the object returned by the function with s. Next, the destructor is called for s. Finally, the destructor is called for the object returned by returnStudent. So the cost of this do-nothing function is two calls to the Student copy constructor and two calls to the Student destructor.

But wait, there's more! A Student object has two string objects within it, so every time you construct a Student object you must also construct two string objects. A Student object also inherits from a Person object, so every time you construct a Student object you must also construct a Person object. A Person object has two additional string objects inside it, so each Person construction also entails two more string constructions. The end result is that passing a StudentStudent copy constructor, one call to the Person copy constructor, and four calls to the string copy constructor. When the copy of the Student object is destroyed, each constructor call is matched by a destructor call, so the overall cost of passing a Student by value is six constructors and six destructors. Because the function returnStudent uses pass-by-value twice (once for the parameter, once for the return value), the complete cost of a call to that function is twelve constructors and twelve destructors!

Passing parameters by reference has another advantage: it avoids what is sometimes called the "slicing problem." When a derived class object is passed as a base class object, all the specialized features that make it behave like a derived class object are "sliced" off, and you're left with a simple base class object. This is almost never what you want. For example, suppose you're working on a set of classes for implementing a graphical window system:

class Window {
public:
  string name() const;             // return name of window
  virtual void display() const;    // draw window and contents
};

class WindowWithScrollBars: public Window {
public:
  virtual void display() const;
};

Now suppose you'd like to write a function to print out a window's name and then display the window. Here's the wrong way to write such a

function: ¤ Item E22, P13

    // a function that suffers from the slicing problem
    void printNameAndDisplay(Window w)
    {
      cout << w.name();
      w.display();
    }
    

Consider what happens when you call this function with a WindowWithScrollBars object: ¤ Item E22, P14

    WindowWithScrollBars wwsb;
    printNameAndDisplay(wwsb);
    

The parameter w will be constructed — it's passed by value, remember? — as a Window object, and all the specialized information that made wwsb act like a WindowWithScrollBars object will be sliced off. Inside printNameAndDisplay, w will always act like an object of class Window (because it is an object of class Window), regardless of the type of object that is passed to the function. In particular, the call to display inside printNameAndDisplay will always call Window::display, never WindowWithScrollBars::display. ¤ Item E22, P15

The way around the slicing problem is to pass w by reference: ¤ Item E22, P16

    // a function that doesn't suffer from the slicing problem
    void printNameAndDisplay(const Window& w)
    {
      cout << w.name();
      w.display();
    }
    
Now w will act like whatever kind of window is actually passed in. To emphasize that w isn't modified by this function even though it's passed by reference, you've followed the advice of Item 21 and carefully declared it to be const; how good of

you. 

Item 24:  Choose carefully between function overloading and parameter defaulting.

 

 

Item 25:  Avoid overloading on a pointer and a numerical type. 

 

The problem can be exterminated, but it requires the use of a late-breaking addition to the language: member function templates (often simply called member templates). Member function templates are exactly what they sound like: templates within classes that generate member functions for those classes. In the case of NULL, you want an object that acts like the expression static_cast<T*>(0) for every type T. That suggests that NULL should be an object of a class containing an implicit conversion operator for every possible pointer type. That's a lot of conversion operators, but a member template lets you force C++ into generating them for you:

    // a first cut at a class yielding NULL pointer objects
    class NullClass {
    public:
      template<class T>                       // generates
        operator T*() const { return 0; }     // operator T* for
    };                                        // all types T; each
                                              // function returns
                                              // the null pointer
    
    const NullClass NULL;             // NULL is an object of
                                      // type NullClass
    
    void f(int x);                    // same as we originally had
    
    void f(string *p);                // ditto
    
    f(NULL);                          // fine, converts NULL to
                                      // string*, then calls f(string*

 

This is a good initial draft, but it can be refined in several ways. First, we don't really need more than one NullClass object, so there's no reason to give the class a name; we can just use an anonymous class and make NULL of that type. Second, as long as we're making it possible to convert NULL to any type of pointer, we should handle pointers to members, too. That calls for a second member template, one to convert 0 to type T C::* ("pointer to member of type T in class C") for all classes C and all types T. (If that makes no sense to you, or if you've never heard of — much less used — pointers to members, relax. Pointers to members are uncommon beasts, rarely seen in the wild, and you'll probably never have to deal with them. The terminally curious may wish to consult Item 30, which discusses pointers to members in a bit more detail.) Finally, we should prevent clients from taking the address of NULL, because NULL isn't supposed to act like a pointer, it's supposed to act like a pointer value, and pointer values (e.g., 0x453AB002) don't have addresses.

 

The jazzed-up NULL definition looks like this:

    const                             // this is a const object...
    class {
    public:
      template<class T>               // convertible to any type
        operator T*() const           // of null non-member
        { return 0; }                 // pointer...
    
     template<class C, class T>      // or any type of null
        operator T C::*() const       // member pointer...
        { return 0; }
    
    private:
      void operator&() const;         // whose address can't be
                                      // taken (see Item 27)...
    }  NULL;                           // and whose name is NULL
    

 

 

 

 

 

 

 

 

 

 

 

 

 

Document Actions