views:

120

answers:

3

Given:

#include <iostream>
using namespace std;

struct Concrete {
  char name[20];
  char quest[20];
  char favorite_color[13];
};

struct Concrete_with_knobs : public Concrete {
  int knobs[100000];
};

Concrete * cradle() {
    return new Concrete_with_knobs;
}

void grave(Concrete *p) {
    delete p;
}

void tomb_of_the_unknown_allocation(void *pv) {
    delete static_cast<int *>(pv);
}

void stress() {
    for (int i = 0; i < 1000000; ++i) {
        Concrete *p = cradle();
        grave(p);
    }
}

void extreme_stress() {
    for (int i = 0; i < 1000000; ++i) {
        Concrete *p = cradle();
        void *pv = p;
        tomb_of_the_unknown_allocation(pv);
    }
}

int main(int, char **) {
    cout << "sizeof(Concrete_with_knobs): " << sizeof(Concrete_with_knobs) << endl;
    cout << "Check your memory." << endl;
    char c;
    cin >> c;
    cout << "Stress." << endl;
    stress();
    cout << "Check your memory." << endl;
    cin >> c;
    cout << "Extreme stress." << endl;
    extreme_stress();
    cout << "Check your memory." << endl;
    cin >> c;
    return 0;
}

Summary:

  • A derived class (Concrete_with_knobs) is 400K larger than its base class (Concrete).
  • cradle creates a Concrete_with_knobs object on the heap, and returns its address as a Concrete pointer.
  • grave takes an address as a Concrete pointer, and performs a delete operation.
  • *tomb_of_the_unknown_allocation* takes an address as a void *, interprets it as the address of an int using static_cast, and performs a delete operation.
  • stress calls cradle and grave a million times.
  • extreme_stress calls cradle and tomb_of_the_unknown_allocation a million times.

If compiled with a conformant C++ compiler, will the system show memory leaks from either stress or extreme_stress? If so, what is an example of a compiler in general release which does in fact produce a leaky binary?

+3  A: 

Very unlikely. Most (all?) C++ compilers simply make the default operator new and operator delete wrap malloc and free, or other memory allocation primitives that normally don't know anything about objects and types, and allocate/release raw chunks of memory (and thus have to store the size within to know how much to release). Naturally, for overloaded operator new & delete, all bets are off.

stress is even more pointless, as any memory allocator worth its salt will simply keep reusing the same memory block again and again, since it's so conveniently sized to match the object being allocated.

Of course, in theory, a conformant C++ compiler might leak on your delete (int*). Or it might format your hard drive. Or it might hack Pentagon and launch an all-out nuclear missile strike. Because that's what U.B. means in ISO C++ - absolutely anything can happen, including nothing - and what you're doing there is definitely U.B.

Pavel Minaev
+1 I'm hoping that, for delete(int *), the compiler will emit the plans for economical renewable energy; I'm an optimist.
Thomas L Holaday
+2  A: 

If you delete via an int pointer, the compiler will not call the destructor for the object being deleted. Since these are PODs with no destructor, that's not a factor.

The other issue is whether the memory will be freed properly. I can't imagine an implementation where it wouldn't since the memory allocation functions underlying new and delete will just be like malloc and free, i.e. type-independent functions that operate on void * pointers.

Pedantically speaking you are invoking undefined behavior, but practically speaking I will eat my hat* if there's a compiler that doesn't handle this sanely.

* I do not have a hat.

John Kugelman
I'm thinking that someone should really write a C++ compiler that does its best to do nasty things on any attempt to tread into U.B. territory, strictly according to the letter of the C++ spec (e.g. fat pointers and checks inserted so that you get noisy failure on attempts to compare pointers to objects from different arrays, strict typechecking for `delete`, etc). If only so that, whenever that question comes up, I could point at it and say, "Yes, there is an ISO C++ compiler that kills a kitten every time you cast a function pointer to `void*`!"
Pavel Minaev
@Pavel: Agreed, although there are a few undefined behaviours which are de facto standards, such as for instance if you write into one member of a union and then read another member, what you see is the value that has the same storage representation. Every compiler does this, and everyone's byteswap routines break if they don't, but it's UB.
Steve Jessop
Old versions of GCC used to have a few examples of that. When it detected certain kinds of UB, it would invoke printf and print some kind of silly message.
jalf
+1 * I do not have a hat.
Justin
+2  A: 

A conformant C++ compiler can do anything it likes with that code. It could format your harddrive, or email your collection of porn to your grandma. It may even delete the objects correctly.

Your program is not legal C++.

Your question can be simplified like this:

Given the following two classes:

struct Base{
  int i;
};

struct Derived : public Base{
  int j;
};

will any conformant C++ compiler leak memory in the following cases:

// 1
Base* b1 = new Derived();
delete b1;
// 2
Base* b2 = new Derived();
void* vp = b2;
delete static_cast<int*>(vp);

Correct? The number of allocations, or the size of each class isn't really relevant to the question.

If so, the answer is simple. In both cases, the result is undefined behavior. According to 5.3.5:3, if the static type of the object being deleted is different from the dynamic type, the static type shall be a base class of the operand's dynamic type and the static type shall have a virtual destructor.

In the first example, the static type (Base) is a base class of the dynamic type (Derived), but it doesn't have a virtual destructor.

In the second case, neither requirement is fulfilled.

So is there a compiler where this won't still appear to work? I don't know of one. The compilers I know of will release the memory as long as the pointer passed to delete points to the same address as the one that was returned from new. And that will be the case in both cases here.

However, you're asking the wrong question. It is not "will it leak memory" that is important, but "will it do anything bad?" It is undefined behavior, so it might do a hell of a lot more than leak memory. An obvious thing it could do is just do a quick type check, and then terminate your program if the types don't match up. Or it could corrupt the heap. There are plenty of things that a conformant compiler could do. Memory leaks are just one option.

Finally, it's worth pointing out that the derived class is not a POD. A POD, by definition, may not derive from another class.

jalf
+1 for the paragraph number. The two million allocations in the supplied source are there to provide experimental support for the theory that a particular compiler author has elected to implement the U.B. in the traditional manner. I understand that a conformant compiler could have counter which allows two million deletes of an object whose dynamic type differs from its static type, and then on the two million and first halts the system, but this is the Problem of Induction.
Thomas L Holaday
The thing about UB is that implementers don't usually "elect to implement it" in any manner. They just ignore it, and then whatever happens, happens. In this case, they identify allocations solely by address, not type, so it turns out to work as long as the correct address is passed to delete. Not because implementers made a conscious decision about this instance of UB, but as a side effect of the decision they made about new/delete in general.
jalf