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,When what's pointed to is constant, some programmers list
// 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
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
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 Student
Student
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