Solution
Let's take the questions one at a time.
-
Consider the
following code:
class B
{
public:
virtual ~B();
void operator delete ( void*, size_t ) throw();
void operator delete[]( void*, size_t ) throw();
void f( void*, size_t ) throw();
};
class D : public B
{
public:
void operator delete ( void* ) throw();
void operator delete[]( void* ) throw();
};
Why do B's operators delete
have a second parameter, whereas D's do not?
The answer is: It's just preference, that's all.
Both are usual deallocation functions, not placement
deletes. (For those keeping score at home, see section
3.7.3.2/2 in the C++ standard.)
However, there's also a memory error lurking in
this underbrush. Both classes provide an operator delete()
and operator delete[](), without providing the
corresponding operator new() and operator
new[](). This is extremely dangerous, because the default
operator new() and operator new[]() are unlikely
to do the right thing. (For example, consider what happens if a
further-derived class provides its own operator new() or
operator new[]() functions.)
Guideline
|
Always provide both
class-specific new (or new[]) and class-specific
delete (or delete[]) if you provide
either.
|
And the second part of the question: Do you see
any way to improve the function declarations?
All flavors of operator new() and
operator delete() are always static functions, even if
they're not declared static. Although C++ doesn't force
you to say "static" explicitly when you declare your own,
it's better to do so anyway, because it serves as a reminder to
yourself as you're writing the code and as a reminder to the next
programmer who has to maintain it.
Guideline
|
Always explicitly
declare operator new() and operator delete() as
static functions. They are never nonstatic member functions.
|
-
Continuing with
the same piece of code: Which operator delete() is called
for each of the following delete expressions? Why, and
with what parameters?
D* pd1 = new D;
delete pd1;
This calls D::operator
delete(void*).
B* pb1 = new D;
delete pb1;
This also calls D::operator
delete(void*). Since B's destructor is virtual, of
course D's destructor is properly called, but the fact
that B's destructor is virtual also implicitly means that
D::operator delete() must be called, even though
B::operator delete() is not (in fact, cannot be)
virtual.
As an aside to those who are interested in how
compilers implement these things: The usual method is that the code
actually generated for every destructor is given an invisible "when
done destroying the object, should I delete it?" flag that is set
appropriately (false when destroying an automatic object,
true when destroying a dynamic object). The last thing the
generated destructor code does is check the flag and, if it's
true, call the correct operator
delete(). This technique automatically ensures the
correct behavior, namely that operator delete() appears to
act "virtually" even though it is a static function and, therefore,
cannot be virtual.
D* pd2 = new D[10];
delete[] pd2;
This calls D::operator
delete[](void*).
B* pb2 = new D[10];
delete[] pb2;
This is undefined behavior. The language
requires that the static type of the pointer that is passed to
operator delete[]() must be the same as its dynamic type.
For more information on this topic, see also Scott Meyers' section,
"Never Treat Arrays Polymorphically" in Meyers99.
Guideline
|
Never treat arrays
polymorphically.
|
Guideline
|
Prefer using
vector<> or deque<> instead of
arrays.
|
-
Are the
following two assignments legal?
typedef void (B::*PMF)(void*, size_t);
PMF p1 = &B::f;
PMF p2 = &B::operator delete;
The first assignment is fine; we're simply
assigning the address of a member function to a pointer to member
function.
The second assignment is illegal because
void operator delete( void*, size_t ) throw() is
not a nonstatic member function of
B, even though as it's written, it may look like one. The
trick here is to remember that operator new() and
operator delete() are always static members, even if
they're not explicitly declared static. It's a good habit to always
declare them static, just to make sure that the fact is obvious to
all programmers reading through your code.
Guideline
|
Always explicitly
declare operator new() and operator delete() as
static functions. They are never nonstatic member functions.
|
-
Are there any
memory-related errors or issues in the following code?
Short answer: Yes, in every case. Some of them
are just a little more subtle than others, that's all.
class X
{
public:
void* operator new( size_t s, int )
throw( bad_alloc )
{
return ::operator new( s );
}
};
This invites a memory leak, because no
corresponding placement delete exists. Similarly
below:
class SharedMemory
{
public:
static void* Allocate( size_t s )
{
return OsSpecificSharedMemAllocation( s );
}
static void Deallocate( void* p, int i )
{
OsSpecificSharedMemDeallocation( p, i );
}
};
class Y
{
public:
void* operator new(size_t s,
SharedMemory&m ) throw( bad_alloc )
{
return m.Allocate( s );
}
This invites a memory leak, because no
operator delete() matches this signature. If an exception
is thrown during construction of an object to be located in memory
allocated by this function, the memory will not be properly freed.
For example, consider the following code:
SharedMemory shared;
...
new (shared) Y; // if Y::Y() throws, memory is leaked
Further, any memory allocated by this
operator new() cannot safely be deleted because the class
does not provide a usual operator delete(). This means
that a base or derived class's operator delete(), or the
global one, will have to try to deal with this deallocation (almost
certainly unsuccessfully, unless you also replace all such
surrounding operator delete's, which would be
onerous and evil).
void operator delete( void* p,
SharedMemory& m,
int i ) throw()
{
m.Deallocate( p, i );
}
};
This Y::operator delete() is useless
because it can never be called.
void operator delete( void* p ) throw()
{
SharedMemory::Deallocate( p );
}
This is a serious error, because the replacement
global operator delete() is going to delete memory
allocated normally by the default ::operator new(), not by
SharedMemory::Allocate(). The best you can hope for is a
quick core dump. Evil.
void operator delete( void* p,
std::nothrow_t& ) throw()
{
SharedMemory::Deallocate( p );
}
The same comment applies again here, but this
time it's slightly more subtle. This replacement operator
delete() will be called only if an expression like "new
(nothrow) T" fails because T's constructor exits with
an exception and will try to deallocate memory not allocated by
SharedMemory::Allocate(). Evil and insidious.
Guideline
|
Always provide both
class-specific new (or new[]) and class-specific
delete (or delete[]) if you provide
either.
|
If you got and understood all of these answers,
then you're definitely on your way to becoming an expert in
memory-management mechanics.
|