Effective C++, 2E | Item 25: Avoid overloading on a pointer and a numerical type Back to Item 24: Choose carefully between function overloading and parameter defaulting. Continue to Item 26: Guard against potential ambiguity. Item 25: Avoid overloading on a pointer and a numerical type. Trivia question for the day: what is zero? More specifically, what will happen here? void f(int x); void f(string *ps); f(0); // calls f(int) orf(string*)? The answer is that 0 is an int a literal integer constant, to be precise so f(int) will always be called. Therein lies the problem, because that's not what people always want. This is a situation unique in the world of C++: a place where people think a call should be ambiguous, but compilers do not. It would be nice if you could somehow tiptoe around this problem by use of a symbolic name, say, NULL for null pointers, but that turns out to be a lot tougher than you might imagine. Your first inclination might be to declare a constant called NULL, but constants have types, and what type should NULL have? It needs to be compatible with all pointer types, but the only type satisfying that requirement is void*, and you can't pass void* pointers to typed pointers without an explicit cast. Not only is that ugly, at first glance it's not a whole lot better than the original situation: void * const NULL = 0; // potential NULL definition f(0); // still calls f(int) f(static_cast(NULL)); // calls f(string*) f(static_cast(0)); // calls f(string*) On second thought, however, the use of NULL as a void* constant is a shade better than what you started with, because you avoid ambiguity if you use only NULL to indicate null pointers: f(0); // calls f(int) f(NULL); // error! type mis-match f(static_cast(NULL)); // okay, calls f(string*) At least now you've traded a runtime error (the call to the "wrong" f for 0) for a compile-time error (the attempt to pass a void* into a string* parameter). This improves matters somewhat (see Item 46), but the cast is still unsatisfying. If you shamefacedly crawl back to the preprocessor, you find that it doesn't really offer a way out, either, because the obvious choices seem to be #define NULL 0 and #define NULL ((void*) 0) and the first possibility is just the literal 0, which is fundamentally an integer constant (your original problem, as you'll recall), while the second possibility gets you back into the trouble with passing void* pointers to typed pointers. If you've boned up on the rules governing type conversions, you may know that C++ views a conversion from a long int to an int as neither better nor worse than a conversion from the long int 0 to the null pointer. You can take advantage of that to introduce the ambiguity into the int/pointer question you probably believe should be there in the first place: #define NULL 0L // NULL is now a long int void f(int x); void f(string *p); f(NULL); // error! ambiguous However, this fails to help if you overload on a long int and a pointer: #define NULL 0L void f(long int x); // this f now takes a long void f(string *p); f(NULL); // fine, calls f(long int) In practice, this is probably safer than defining NULL to be an int, but it's more a way of moving the problem around than of eliminating it. 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(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 // 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 // convertible to any type operator T*() const // of null non-member { return 0; } // pointer... template // 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 This is truly a sight to behold, though you may wish to make a minor concession to practicality by giving the class a name after all. If you don't, compiler messages referring to NULL's type are likely to be pretty unintelligible. For another example of how member templates can be useful, take a look at Item M28. An important point about all these attempts to come up with a workable NULL is that they help only if you're the caller. If you're the author of the functions being called, having a foolproof NULL won't help you at all, because you can't compel your callers to use it. For example, even if you offer your clients the space-age NULL we just developed, you still can't keep them from doing this, f(0); // still calls f(int), // because 0 is still an int and that's just as problematic now as it was at the beginning of this Item. As a designer of overloaded functions, then, the bottom line is that you're best off avoiding overloading on a numerical and a pointer type if you can possibly avoid it. Back to Item 24: Choose carefully between function overloading and parameter defaulting. Continue to Item 26: Guard against potential ambiguity.