|
To New, Perchance To Throw[1], Part 1This article appeared in C/C++ Users Journal, 19(3), March 2001.
In this column and the next, I want to state and justify just two main pieces of advice:
Some of this advice may be surprising, so let's examine the reasons and rationale that lead to it. This article focuses on the first point; next time we'll consider the second. For simplicity, I'm not going to mention the array forms of new specifically; what's said about the single-object forms applies correspondingly to the array forms. In-Place, Plain, and Nothrow NewThe C++ standard provides three forms of new, and allows any number of additional overloads. One useful form is in-place new, which constructs an object at an existing memory address without allocating new space. For example: // Example 1: Using in-place new, an "explicit constructor call" new (p) T; // construct the T at address p, probably calls The standard also supplies "plain old new" which doesn't take any special additional parameters, and nothrow new which does. Here's a complete list of the operator new overloads supplied in Standard C++: // The standard-provided overloads of operator new void* ::operator new(std::size_t size, const std::nothrow_t&) throw(); void* ::operator new(std::size_t size, void* ptr) throw(); Programs are permitted to replace all but the last form with their own versions. All of these standard functions live in global scope, not in namespace std. In brief, Table 1 summarizes the major characteristics of the standard versions of new:
Table 1: Comparison of the standard versions of new Here is an example showing some ways to use these versions of new: // Example 2: Using various indigenous and user-supplied new (42, 3.14159, "xyzzy") T; new (std::nothrow) T; In each case shown in Examples 1 and 2, the parameters inside the brackets in the new-expression turn into additional parameters tacked onto the call to operator new(). Of course, unlike the case in Example 1, the cases in Example 2 probably do allocate memory in one way or another, rather than use some existing location. Class-Specific NewBesides letting programs replace some of the global operators new, C++ also lets classes provide their own class-specific versions. When reading Examples 1 and 2, did you notice the word "probably" in two of the comments? They were: new (p) T; // construct the T at address p, probably calls new (std::nothrow) T; The "probably" is because the operators invoked may not necessarily be the ones at global scope, but may be class-specific ones. To understand this clearly, notice two interesting interactions between class-specific new and global new:
So in the two code lines repeated above, it's possible that T (or one of T's base classes) provides its own versions of one or both operators new being invoked here, and if so then those are the ones that will get used. Here is a simple example of providing class-specific new, where we just provide our own versions of all three global flavors: // Example 3: Sample class-specific versions of new X* p1 = new X; // calls 1 X* p2 = new (std::nothrow) X; // calls 2 void* p3 = /* some valid memory that's big enough for an X */ I put an exclamation point after the third call to again draw attention to the funky fact that you can provide a class-specific version of in-place new even though you can't replace the global one. A Name Hiding SurpriseThis, finally, brings us to the reason I've introduced all of this machinery in the first place, namely the name hiding problem: // Example 4: Name hiding "news" class Derived : public Base Derived* p1 = new Derived; // ERROR: no match Derived* p2 = new (std::nothrow) Derived; // ERROR: no match void* p3 = /* some valid memory that's big enough for a Derived */ Derived* p4 = new (FastMemory()) Derived; // calls 4 Most of us are familiar with the name hiding problem in other contexts, such as a name in a derived class hiding one in the base class, but it's worth remembering that name hiding can crop up for operator new too. Remember how name lookup works: In brief, the compiler starts in the current scope (here, in Derived's scope), and looks for the desired name (here, operator new); if no instances of the name are found, it moves outward to the next enclosing scope (in Base's and then global scope) and repeats. Once it find a scope containing at least one instance of the name (in this case, Base's scope), it stops looking and works only with the matches it has found, which means that further outer scopes (in this case, the global scope) are not considered and any functions in them are hidden; instead, the compiler looks at all the instances of the name it has found, selects a function using overload resolution, and finally checks access rules to determine whether the selected function can be called. The outer scopes are ignored even if none of the overloads found has a compatible signature, meaning that none of them could possibly be the right one; the outer scopes are also ignored even if the signature-compatible function that's selected isn't accessible. That's why name hiding works the way it does in C++. (For more details about name lookup and name hiding, see Item 34 in Exceptional C++.[3]) What this means is that if a class C, or any of its base classes, contains a class-specific operator new() with any signature, that function will hide all of the global ones and you won't be able to write normal new-expressions for C that intend to use the global versions. The only reasonable way to re-enable the global ones is for C to provide the necessary passthrough functions itself - calling code must otherwise know to write globally-qualified new-expressions to select a global operator new(), This leads to a few interesting conclusions, best expressed as a coding and design guideline. Scott Meyers covers part of the first bullet in Item 9 of Effective C++[4], but the other points are as important. Guideline: If you provide any class-specific new, then also:
Note that these passthroughs can't be simulated with a using declaration, such as "using ::operator new;". The only place such a using declaration would be helpful would be inside the definition for class C, but it's illegal there; within a class, you can only write using declarations that bring in names from base classes, not other names such as global names or names from other classes. Requiring that the calling code add the using declaration itself would not only be onerous, but wouldn't even help because we may not be able to modify it; some of the calling code would be inside what to us are read-only modules like third-party libraries, or even inside the C++ standard library if we were trying to provide the standard containers with access to in-place new. SummaryNext time, we'll delve deeper into the question of what operator new() failures mean, and how best to detect and handle them. Along the way, we'll see why it can be a good idea to avoid using new(nothrow) - perhaps most surprisingly, we'll also see that, on certain popular real-world platforms, memory allocation failures usually don't even manifest in the way the standard says they must! Stay tuned.
Notes1. With apologies to the Bard - meaning either Shakespeare or Bacon, depending which version of history you happen to prefer. 2. After operator new() is done, the object's constructor will be invoked and of course that constructor operation might still fail, but we're not worried about that here. Here we're analyzing specifically whether or not operator new() itself can fail. 3. Herb Sutter. Exceptional C++ (Addison-Wesley, 2000). 4. Scott Meyers. Effective C++, 2nd edition (Addison-Wesley, 1997). |
Copyright © 2009 Herb Sutter |