Sutter’s Mill: To New, Perchance To Throw [1] (Part 1 of 2)

If you're going to overload operator new, make sure you get the right one, and that the compiler picks the right ones as well.


March 01, 2001
URL:http://www.drdobbs.com/sutters-mill-to-new-perchance-to-throw-1/184401369

March 2001/Sutter's Mill


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 new

The 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
//
// grab a sufficient amount of
// raw memory
void* p = ::operator new(sizeof(T));
    
// construct the T at address p,
// probably calls
// ::operator new(std::size_t, void*)
//     throw()
new (p) T;  

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 (there are also
// corresponding ones for array
// new[]):

// usual plain old boring new
// usage: new T
void*
::operator new(std::size_t size)
   throw(std::bad_alloc);
    
// nothrow new
// usage: new (std::nothrow) T
void*
::operator new(std::size_t size,
   const std::nothrow_t&) throw();
    
// in-place or "put-it-there" new
// usage: new (ptr) T
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.

Here is an example showing some ways to use these versions of new:

// Example 2: Using various
// indigenous and user-supplied
// overloads of new
//

// calls some user-supplied
//
// operator new(std::size_t,
//    FastMemory&)
//
// (or something similar, with
// argument type conversions),
// presumably to select a custom
// memory arena
new (FastMemory()) T;
    
// calls some user-supplied
//
// operator new(std::size_t,
//    int, double, const char*)
//
// (or something similar, with
// argument type conversions)
new (42, 3.14159, "xyzzy") T;
    
// probably calls the standard or
// some user-supplied
//
// ::operator new(std::size_t,
//   const std::nothrow_t&) throw()
//
new (std::nothrow) T;

In each case shown in Examples 1 and 2, the parameters inside the parentheses 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 New

Besides 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:

// construct the T at address p,
// probably calls
// ::operator new(std::size_t, void*)
//    throw()
//
new (p) T;  
            
// probably calls the standard or
// some user-supplied
// ::operator new(std::size_t,
//    const std::nothrow_t&) throw()
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
//
class X
{
public:
  static void* operator new( std::size_t )
     throw();                    // 1

  static void* operator new( std::size_t,
     const std::nothrow_t& )
     throw();                    // 2

  static void* operator new( std::size_t, void* )
     throw();                    // 3
};

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 */
   new (p3) X;   // calls 3 (!)

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 Surprise

This, 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 Base
{
public:
  static void* operator new( std::size_t,
     const FastMemory& );         //4
};

class Derived : public Base
{
  // ...
};

Derived* p1 = new Derived; // ERROR: no match

// ERROR: no match
Derived* p2 = 
   new (std::nothrow) Derived;

void* p3 = /* some valid memory
              that's big enough for  
              a Derived */

// ERROR: no match
new (p3) Derived;

// calls 4
Derived* p4 = 
   new (FastMemory()) Derived;

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, 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++ [2].)

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++ [3], but the other points are as important.

Guideline: If you provide any class-specific new, then also:

Summary

Next 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.

Notes

[1] With apologies to the Bard — meaning either Shakespeare or Bacon, depending which version of history you happen to prefer.

[2] Herb Sutter. Exceptional C++ (Addison-Wesley, 2000).

[3] Scott Meyers. Effective C++, Second Edition (Addison-Wesley, 1997).

Herb Sutter is an independent consultant and secretary of the ISO/ANSI C++ standards committee. He can be reached at [email protected].

March 2001/Sutter's Mill/Table 1

Table 1: Comparison of the standard versions of new

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.