3

I am playing with the code

struct A {
    char a[20000];
    A() { a[0] = 'A'; }
    ~A() {}
};
struct B : A {
    char a[20000];
    B() { a[0] = 'B'; }
    ~B() {}
};
int main() {
    A *pA = new A;
    A *pB = new B;
    delete pA;
    delete pB;
    return 0;
}

Some people wrote (why do we need a virtual destructor with dynamic memory?) that it should cause a memory leak but it doesn't. I used g++, then valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose --log-file=valgrind-out.txt and get

HEAP SUMMARY:
in use at exit: 0 bytes in 0 blocks
total heap usage: 3 allocs, 3 frees, 132,704 bytes allocated
All heap blocks were freed -- no leaks are possible

I know that some old compilers had problems when they try to free memory in similar situations but it looks like that the modern C++ can free memory seamlessly in this case. So I am curios how is it possible? Maybe delete uses information provided by OS for a given allocated memory block?

EDIT. It is still unclear for me what exactly can cause the UB for delete *pB if we have empty destructors and constructors. An answer for a question (Missing Virtual Destructor Memory Effects) shows that there are no any UB.

vollitwr
  • 429
  • 2
  • 7
  • 1
    Undefined behavior is undefined. It might do just what you want, and it might do something else. – Pete Becker Jun 27 '19 at 14:19
  • Thank you very much for the link. – vollitwr Jun 27 '19 at 14:37
  • 1
    "Maybe delete uses information provided by OS for a given allocated memory block?" No. It's an implementation detail, which may vary by platform. To the best of my knowledge, no platforms use information provided by the OS for something like this. On my platform, the allocator has some bookkeeping, which tracks of how many bytes are allocated for the object in the heap for that pointer (additional memory is allocated for fenceposts in debug builds, and there are separate heaps for `new`, `new[]`, and `malloc`). – Eljay Jun 27 '19 at 14:44

3 Answers3

5

I assume you're already aware that delete pB is undefined behaviour. But, why doesn't it leak memory on your machine? That has to do with how your implementation's dynamic memory management works.

Here is one possibility: on some implementations, the default global operator new and operator delete work by calling the C library malloc and free functions. But free needs to be able to do its job when it's just passed a void* with no type or size information, so that implies that malloc must "write down" the size somewhere before it returns. In your program, the call to new B may cause malloc to write down the size of B, so that when free is later passed the pointer, it knows exactly how many bytes to free.

Brian Bi
  • 91,815
  • 8
  • 136
  • 249
4

You don't have any memory allocation on A or B, so why a leak should be there? A leak is there when the incorrect destructor is called (due to not being virtual).

If the destructor need not do anything, because there was nothing to clear anyway, no leak would exist.

Change a[20000] to *a and new/delete it, and see what happens.

Btw others say that there is undefined behaviour and there is one, but this question is more related to the fact that the OP does not understand how a destructor works. It's not an exact duplicate of the linked question.

struct A {
    char* a;
    A() { a = new char[20000]; a[0] = 'A'; }
    ~A() { delete[] a;}
};
struct B : A {
    char* b;
    B() : A() { b = new char[20000]; b[0] = 'B'; }
    ~B() { delete[] b;}
};
int main() {
    A *pA = new A;
    A *pB = new B;
    delete pA;
    delete pB;
    return 0;
}
Michael Chourdakis
  • 8,819
  • 2
  • 32
  • 61
  • But examples at the mentioned page also miss any destructor with memory deallocation. – vollitwr Jun 27 '19 at 14:11
  • 3
    The destructor does not clear the memory of the deleted object; This is what delete/new does. No matter which destructor is called, the original memory *will* be released; it's the members that need destruction that are problematic. – Michael Chourdakis Jun 27 '19 at 14:12
  • @vollitwr The "examples at the mentioned page" says _"Please assume some constructors destructors."_ It implies that `int *x` and `int *y` are owning pointers to dynamically allocated memory which the constructors and destructors allocate and deallocate, respectively. Your initial example does nothing like that - the char `a[2000]` is not dynamically allocated but part of the class footprint. – Max Langhof Jun 27 '19 at 14:14
  • @vollitwr For what it's worth, I do agree that omitting the constructors/destructors in the answer you linked to is confusing. – Max Langhof Jun 27 '19 at 14:20
  • @MaxLanghof The comments for the second example clearly states that "You need a virtual destructor in the base class when you attempt to delete a derived-class object through a base-class pointer". – vollitwr Jun 27 '19 at 14:22
  • This is undefined behavior. Anything can happen. In particular, no memory leak can happen. – SergeyA Jun 27 '19 at 14:23
  • I suggest OP to look at other answers. – SergeyA Jun 27 '19 at 14:24
  • @SergeyA not exactly related to UD this one. It's more that there is no memory allocation whatsoever that causes the no leak, not the UD. – Michael Chourdakis Jun 27 '19 at 14:24
  • @MichaelChourdakis, nope. This is exactly related to UB. I can easily put together for you a conforming implementation on which exactly this code will leak like sieve. It is just so happens that common implementations of `new` call `malloc`. – SergeyA Jun 27 '19 at 14:26
  • @vollitwr You do always need the virtual destructor to have no UB. A memory leak is a very specific case of UB. So, you can violate this rule and get UB without a memory leak. But in specific cases the UB may manifest as a memory leak (in particular in the kind of case this answer shows). Does that clear things up? – Max Langhof Jun 27 '19 at 15:00
  • @MaxLanghof It is unclear for me what exactly can cause the UB if we have empty destructors and constructors. IMHO the latest standards (17, 20) don't clearly point at the UB for this case. – vollitwr Jun 27 '19 at 16:37
  • @vollitwr check 7.6.2.8/3 in latest draft. – SergeyA Jun 27 '19 at 16:56
  • 1
    @vollitwr For your convenience: http://eel.is/c++draft/expr.unary#expr.delete-3 – Max Langhof Jun 28 '19 at 07:43
  • @MaxLanghof Thank you very much but this document has an addiotion **and the selected deallocation function (see below) is not a destroying operator delete** which shows that my example doesn't have any UB because it does use a destroying operator delete. However it doesn't solve mystery how **delete pB** deallocate two chunks of memory having only one pointer. – vollitwr Jun 28 '19 at 08:20
  • @vollitwr "destroying operator delete" is defined [here](http://eel.is/c++draft/basic.stc.dynamic.deallocation#2). There is no destroying operator delete in your code. – Max Langhof Jun 28 '19 at 08:27
  • @MaxLanghof Formally you are right but there is a material (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0722r1.html) about the destroying operator delete which shows that the whole purpose of this destructor is to dispatch calls to **non-empty** destructors. So all this UB are rather appliable only if we have non-empty destructors. Indeed I am not 100% sure. – vollitwr Jun 28 '19 at 10:02
  • @vollitwr `Destroying operator delete`s are completely irrelevant to this discussion, so I'm unsure why you talk about their purpose here. To be abundantly clear: Since you do **not** have a destroying operator delete, the standard essentially says _"In a single-object delete expression, if the static type of the object to be deleted is different from its dynamic type and , the static type shall be a base class of the dynamic type of the object to be deleted **and the static type shall have a virtual destructor** or the behavior is undefined."_ Your code therefore has UB. – Max Langhof Jun 28 '19 at 11:13
  • @vollitwr In other words: If you _had_ a destroying operator delete (you do not) then it would be up to you to properly handle static vs dynamic delete issues (see the `WidgetKind` example in P0722R1). If you do _not_ have a destroying operator delete (your case) then it is your responsibility to only perform polymorphic deletes (static type != dynamic type) on related polymorphic types. That requires a virtual destructor in the base class. Whether the relevant destructors do anything or not is irrelevant. – Max Langhof Jun 28 '19 at 11:18
  • @MaxLanghof Thank you. Sorry that I could not clearly show my point. Our discussion goes far away from it. My initial point was about the mystery of the implementation of g++ compiler which works without a memory leak for my code. I am also curious what exactly can the UB mean for my code. Later I assumed that if a compiler can work perfectly for such code it would also be possible to support such behaviours in the C++ standard. – vollitwr Jun 28 '19 at 11:51
  • @MaxLanghof I have just found an answer (https://stackoverflow.com/questions/31664064/missing-virtual-destructor-memory-effects) that shows that my example doesn't cause the UB. – vollitwr Jun 28 '19 at 14:16
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/195702/discussion-between-max-langhof-and-vollitwr). – Max Langhof Jun 28 '19 at 14:31
4

Why absence of a virtual destructor doesn't cause a memory leak?

Because behaviour of destroying an object through a pointer to a base class whose destructor is not virtual is undefined. When behaviour is undefined, nothing is guaranteed. For example, there is no guarantee that a memory would be leaked.

That said, it is unclear why would there be an expectation of a memory leak. If you take a look at the destructor of B, you'll notice that it does nothing - the body is empty and the member has trivial destructor. There's no reason to expect that not running a function that does nothing would result in a memory leak.

eerorika
  • 181,943
  • 10
  • 144
  • 256