views:

743

answers:

3

I've recently come across this rant.

I don't quite understand a few of the points mentioned in the article:

  • The author mentions the small annoyance of delete vs delete[], but seems to argue that it is actually necessary (for the compiler), without ever offering a solution. Did I miss something?
  • In the section 'Specialized allocators', in function f(), it seems the problems can be solved with replacing the allocations with: (omitting alignment)

    // if you're going to the trouble to implement an entire Arena for memory,
    // making an arena_ptr won't be much work. basically the same as an auto_ptr,
    // except that it knows which arena to deallocate from when destructed.
    arena_ptr<char> string(a); string.allocate(80);
    // or: arena_ptr<char> string; string.allocate(a, 80);
    arena_ptr<int> intp(a); intp.allocate();
    // or: arena_ptr<int> intp; intp.allocate(a);
    arena_ptr<foo> fp(a); fp.allocate();
    // or: arena_ptr<foo>; fp.allocate(a);
    // use templates in 'arena.allocate(...)' to determine that foo has
    // a constructor which needs to be called. do something similar
    // for destructors in '~arena_ptr()'.
    
  • In 'Dangers of overloading ::operator new[]', the author tries to do a new(p) obj[10]. Why not this instead (far less ambiguous):

    obj *p = (obj *)special_malloc(sizeof(obj[10]));
    for(int i = 0; i < 10; ++i, ++p)
        new(p) obj;
    
  • 'Debugging memory allocation in C++'. Can't argue here.

The entire article seems to revolve around classes with significant *constructors* and destructors located in a custom memory management scheme. While that could be useful, and I can't argue with it, it's pretty limited in commonality.

Basically, we have placement new and per-class allocators -- what problems can't be solved with these approaches?

Also, in case I'm just thick-skulled and crazy, in your ideal C++, what would replace operator new? Invent syntax as necessary -- what would be ideal, simply to help me understand these problems better.

+4  A: 

Well, the ideal would probably be to not need delete of any kind. Have a garbage-collected environment, let the programmer avoid the whole problem.

The complaints in the rant seem to come down to

  1. "I liked the way malloc does it"
  2. "I don't like being forced to explicitly create objects of a known type"

He's right about the annoying fact that you have to implement both new and new[], but you're forced into that by Stroustrups' desire to maintain the core of C's semantics. Since you can't tell a pointer from an array, you have to tell the compiler yourself. You could fix that, but doing so would mean changing the semantics of the C part of the language radically; you could no longer make use of the identity

*(a+i) == a[i]

which would break a very large subset of all C code.

So, you could have a language which

  • implements a more complicated notion of an array, and eliminates the wonders of pointer arithmetic, implementing arrays with dope vectors or something similar.

  • is garbage collected, so you don't need your own delete discipline.

Which is to say, you could download Java. You could then extend that by changing the language so it

  • isn't strongly typed, so type checking the void * upcast is eliminated,

...but that means that you can write code that transforms a Foo into a Bar without the compiler seeing it. This would also enable ducktyping, if you want it.

The thing is, once you've done those things, you've got Python or Ruby with a C-ish syntax.

I've been writing C++ since Stroustrup sent out tapes of cfront 1.0; a lot of the history involved in C++ as it is now comes out of the desire to have an OO language that could fit into the C world. There were plenty of other, more satisfying, languages that came out around the same time, like Eiffel. C++ seems to have won. I suspect that it won because it could fit into the C world.

Charlie Martin
B-b-b-but deterministic finalizers are the essence, the heart, the soul of programming!
Thomas L Holaday
I was going to reply, but the more I think of it, the more I want to just leave that sentence as it is, to be admired.
Charlie Martin
Or, perhaps, as a warning to others.
Charlie Martin
Would you make ornate shrines for Brahma and Vishnu, then try to coax Shiva into running anonymously in the background? It does not sound kosher.
Thomas L Holaday
[I think rwars should have plenty of r.]
Thomas L Holaday
Man, you're twisted. I *like* it.
Charlie Martin
So it's okay to just write off everything that guy says?
zildjohn01
No, you should learn from it. He's making a collection of requirements on what *he* would like. C++ isn't that language. What is? And -- as with strong typing and new/new[] -- why did someone else make different choices?
Charlie Martin
+2  A: 

The rant, IMHO, is very misleading and it seems to me that the author does understand the finer details, it's just that he appears to want to mislead. IMHO, the key point that shows the flaw in argument is the following:

void* operator new(std::size_t size, void* ptr) throw();

The standard defines that the above function has the following properties:

Returns: ptr.

Notes: Intentionally performs no other action.

To restate that - this function intentionally performs no other action. This is very important, as it is the key to what placement new does: It is used to call the constructor for the object, and that's all it does. Notice explicitly that the size parameter is not even mentioned.

For those without time, to summarise my point: everything that 'malloc' does in C can be done in C++ using "::operator new". The only difference is that if you have non aggregate types, ie. types that need to have their destructors and constructors called, then you need to call those constructor and destructors. Such types do not explicitly exist in C, and so using the argument that "malloc does it better" is not valid. If you have a struct in 'C' that has a special "initializeMe" function which must be called with a corresponding "destroyMe" then all points made by the author apply equally to that struct as they do to a non-aggregate C++ struct.

Taking some of his points explicitly:

To implement multiple inheritance, the compiler must actually change the values of pointers during some casts. It can't know which value you eventually want when converting to a void * ... Thus, no ordinary function can perform the role of malloc in C++--there is no suitable return type.

This is not correct, again ::operator new performs the role of malloc:

class A1 { };
class A2 { };
class B : public A1, public A2 { };

void foo () {
    void * v = ::operator new (sizeof (B));
    B * b = new (v) B();  // Placement new calls the constructor for B.
    delete v;

    v = ::operator new (sizeof(int));
    int * i = reinterpret_cast <int*> (v);
    delete v'
}

As I mention above, we need placement new to call the constructor for B. In the case of 'i' we can cast from void_ to int_ without a problem, although again using placement new would improve type checking.

Another point he makes is about alignment requirements:

Memory returned by new char[...] will not necessarily meet the alignment requirements of a struct intlist.

The standard under 3.7.3.1/2 says:

The pointer returned shall be suitably aligned so that it can be converted to a pointer of any complete object type and then used to access the object or array in the storage allocated (until the storage is explicitly deallocated by a call to a corresponding deallocation function).

That to me appears pretty clear.

Under specialized allocators the author describes potential problems that you might have, eg. you need to use the allocator as an argument to any types which allocate memory themselves and the constructed objects will need to have their destructors called explicitly. Again, how is this different to passing the allocator object through to an "initalizeMe" call for a C struct?

Regarding calling the destructor, in C++ you can easily create a special kind of smart pointer, let's call it "placement_pointer" which we can define to call the destructor explicitly when it goes out of scope. As a result we could have:

template <typename T>
class placement_pointer {
  // ...
  ~placement_pointer() {
    if (*count == 0) {
      m_b->~T();
    }
  }
  // ...
  T * m_b;
};

void
f ()
{
  arena a;

  // ...
  foo *fp = new (a) foo;           // must be destroyed
  // ...
  fp->~foo ();

  placement_pointer<foo> pfp = new (a) foo; // automatically !!destructed!!
  // ...
}

The last point I want to comment on is the following:

g++ comes with a "placement" operator new[] defined as follows:

inline void *
operator new[](size_t, void *place)
{
  return place;
}

As noted above, not just implemented this way - but it is required to be so by the standard.

Let obj be a class with a destructor. Suppose you have sizeof (obj[10]) bytes of memory somewhere and would like to construct 10 objects of type obj at that location. (C++ defines sizeof (obj[10]) to be 10 * sizeof (obj).) Can you do so with this placement operator new[]? For example, the following code would seem to do so:

obj *
f ()
{
  void *p = special_malloc (sizeof (obj[10]));
  return new (p) obj[10];       // Serious trouble...
}

Unfortunately, this code is incorrect. In general, there is no guarantee that the size_t argument passed to operator new[] really corresponds to the size of the array being allocated.

But as he highlights by supplying the definition, the size argument is not used in the allocation function. The allocation function does nothing - and so the only affect of the above placement expression is to call the constructor for the 10 array elements as you would expect.

There are other issues with this code, but not the one the author listed.

Richard Corden
A: 

description of the rules of implementation of NEW Expressions
Related link Evaluation order of new expression?

lsalamon