Explicit call to destructor
Asked Answered
H

4

25

I stumbled upon the following code snippet:

#include <iostream>
#include <string>
using namespace std;
class First
{
    string *s;
    public:
    First() { s = new string("Text");}
    ~First() { delete s;}
    void Print(){ cout<<*s;}
};

int main()
{
    First FirstObject;
    FirstObject.Print();
    FirstObject.~First();
}

The text said that this snippet should cause a runtime error. Now, I wasn't really sure about that, so I tried to compile and run it. It worked. The weird thing is, despite the simplicity of the data involved, the program stuttered after printing "Text" and only after one second it completed.

I added a string to be printed to the destructor as I was unsure if it was legal to explicitly call a destructor like that. The program printed twice the string. So my guess was that the destructor is called twice as the normal program termination is unaware of the explicit call and tries to destroy the object again.

A simple search confirmed that explicitly calling a destructor on an automated object is dangerous, as the second call (when the object goes out of scope) has undefined behaviour. So I was lucky with my compiler (VS 2017) or this specific program.

Is the text simply wrong about the runtime error? Or is it really common to have runtime error? Or maybe my compiler implemented some kind of warding mechanism against this kind of things?

Hayashi answered 12/11, 2018 at 12:31 Comment(16)
The C++ standard never guarantess a runtime error (it's always undefined behavior), so the text was definitely wrongPieter
@UnholySheep, I don't know that I'd say that. For example, an exception leaving a noexcept function is a guaranteed call to std::terminate, which I'd classify as a runtime error.Urchin
@Urchin good point, I was only thinking about the described case (and I've seen way too many texts claim that code that invokes UB will always cause a runtime error/segmentation fault)Pieter
How does it print the string twice? It will deallocate it twice, because the destructor will be called when the object is destroyed.Ceil
The runtime error definitely occurs: the destructor is called twice, it's an error and it happens at runtime. I bet you can "catch" it if you run your test in debug mode. Why is no message popping? What does VC2017 do in release mode when terminating your app knowing that the last two things it does are deleting the same pointer? Is there some optimization that hides/fixes your error by mistake? You should probably ask MS support...Primogenial
@Primogenial You might want to provide your own answer; be warned though: you clearly state the opposite of what the Standard mandates.Czechoslovakia
@Czechoslovakia I'm quite sure you agree that what the standard mandates and what MS compiler does is unfortunately not always the same thing. I still consider deleting twice a pointer is an error, and - because it happens at runtime - it implies it's a runtime error (unless I missed the definition?). Also, the fact that my debugger gets angry supports my idea, but again, all debuggers don't necessarily behave the same way...Primogenial
@Primogenial I do. And a technical answer about msvc in particular would be welcome. If it's good, i'll try and be the first to upvote.Czechoslovakia
@MatthieuBrucher I think they added a print statement to the destructor.Oxime
The reason the message doesn't appear immediately is probably because you aren't flushing the cout buffer. You need to call cout << std::flush to flush the buffer (or cout << std::endl which calls flush).Fucus
@MatthieuBrucher deallocation and destruction are separate things. They usually occur at the same time but there are conditions when they do not.Dumpcart
I know? I don't think I said anything to point to the opposite.Ceil
@MatthieuBrucher you said "It will deallocate it twice". But now looking back you were talking about the member variable s, not the object itself. My mistake.Dumpcart
@markeransom, no worriesCeil
@Andrea Bocco If an answer suits you, please accept it by clicking the checkmark under its score. I see you asked 9 questions on SO and accepted none. You might want to go back to them and accept the answers you consider solved your problems. This is important because this is how StackOverflow works.Czechoslovakia
Hi Adrea. I've noticed you haven't accepted any of the answers to your questions. Maybe you don't know how it works. When you consider an answer to solve your problem/address your question/etc., you can click the checkmark under the answer scrore to promote it as "the accepted answer". It gives 15rep to its author and 2rep to you. More about accepted answer here: What does it mean when an answer is "accepted"?.Czechoslovakia
C
37

A simple search confirmed that explicitly calling a destructor on an automated object is dangerous, as the second call (when the object goes out of scope) has undefined behaviour.

That is true. Undefined Behavor is invoked if you explicitly destroy an object with automatic storage. Learn more about it.

So I was lucky with my compiler (VS 2017) or this specific program.

I'd say you were unlucky. The best (for you, the coder) that can happen with UB is a crash at first run. If it appears to work fine, the crash could happen in January 19, 2038 in production.

Is the text simply wrong about the runtime error? Or is it really common to have runtime error? Or maybe my compiler implemented some kind of warding mechanism against this kind of things?

Yes, the text's kinda wrong. Undefined behavior is undefined. A run-time error is only one of many possibilities (including nasal demons).

A good read about undefined behavor: What is undefined behavor?

Czechoslovakia answered 12/11, 2018 at 12:48 Comment(3)
"Undefined Behavor is invoked when you explicitly destroy an object with automatic storage" - More like it's invoked when the object reaches the end of its natural lifetime without being reincarnated. The destructor call itself is not UB per se.Perforce
More precisely, 03:14:08 UTC on 19 January 2038 :DViva
@StoryTeller That's true. I wanted to avoid specifics. I've fixed it.Czechoslovakia
I
17

No this is simply undefined behavior from the draft C++ standard [class.dtor]p16:

Once a destructor is invoked for an object, the object no longer exists; the behavior is undefined if the destructor is invoked for an object whose lifetime has ended ([basic.life]). [ Example: If the destructor for an automatic object is explicitly invoked, and the block is subsequently left in a manner that would ordinarily invoke implicit destruction of the object, the behavior is undefined. — end example  

and we can see from the defintion of undefined behavior:

behavior for which this document imposes no requirements

You can have no expectations as to the results. It may have behaved that way for the author on their specific compiler with specific options on a specific machine but we can't expect it to be a portable nor reliable result. Althought there are cases where the implementation does try to obtain a specific result but that is just another form of acceptable undefined behavior.

Additionally [class.dtor]p15 gives more context on the normative section I quote above:

[ Note: Explicit calls of destructors are rarely needed. One use of such calls is for objects placed at specific addresses using a placement new-expression. Such use of explicit placement and destruction of objects can be necessary to cope with dedicated hardware resources and for writing memory management facilities. For example,

void* operator new(std::size_t, void* p) { return p; }
struct X {
  X(int);
  ~X();
};
void f(X* p);

void g() {                      // rare, specialized use:
  char* buf = new char[sizeof(X)];
  X* p = new(buf) X(222);       // use buf[] and initialize
  f(p);
  p->X::~X();                   // cleanup
}

— end note  ]

Ilke answered 12/11, 2018 at 14:15 Comment(0)
D
10

Is the text simply wrong about the runtime error?

It is wrong.

Or is it really common to have runtime error? Or maybe my compiler implemented some kind of warding mechanism against this kind of things?

You cannot know, and this is what happens when your code invokes Undefined Behavior; you don't know what will happen when you execute it.

In your case, you were (un)lucky* and it worked, while for me, it caused an error (double free).


*Because if you received an error you would start debugging, otherwise, in a large project for example, you might missed it...

Danny answered 12/11, 2018 at 12:51 Comment(0)
T
0

Its has been a while since this question, but I think I can contribute a bit more.

First, when an object goes at of scope, its destructor is invoked. Here you are explicitly calling the destructor once, so then it is invoked a second time.

On the second time, it deletes again the internal s pointer, which was already freed. That corrupts the internal memory structs, so it may crash now or it may crash later. You don't know.

It is always good practice to really check and clean data before releasing it. I would have written the destructor like this :

~First() 
{ 
  if (s)
  {
  delete s;
  s = 0;
  }
}

Then you would have been protected from this error.

It is not necessarily bad to manually invoke a destructor. Sometimes it is needed. Let's say you are working on an embedded device, where you don't want to use dynamic allocation constantly. At startup you could allocate a big chunk of memory, and then construct/destroy objects on it.

Let's say you have a raw pointer, pointing to some raw buffer inside that big chunk, that you are going to use as your First object. The size of that buffer has to be at least sizeof(First).

When you want to build the object you use placement new :

new(p) First();

When you want to destroy it, you invoke :

p->~First();

That way you never release the memory, just construct/destruct objects over the preallocated buffer.

Just a reminder, about what new/delete do :

new will allocate memory, and then it will invoke the constructor on it. delete will invoke the destructor and then free the memory.

If you already have the memory, you can just invoke constructor and then destructor, without allocating or freeing.

Being said that, your code would have worked if you had built it again :

int main()
{
    First FirstObject;
    FirstObject.Print();
    FirstObject.~First();
    new(&FirstObject) First(); // constructing the object a second time
}
Tierney answered 29/3, 2023 at 11:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.