-9

Recently I have spotted one peculiar behaviour in direct Call to Destructor, that IMU worth of talking.

consider the following simple code:

#include <iostream>

class S {
public:
   S() = default;
   ~S() { i=10; }
   int i{100};
};

int main()
{
   S s;
   do {
      std::cout << "Before foo: " << s.i;
      s.~S();
      std::cout << "; After: " << s.i << std::endl;
   } while (false);
   return 0;
}

Running:

$g++ -O3 ./d.cpp -o d
$./d
Before foo: 100; After: 0
$g++ -O0 ./d.cpp -o d
$./d
Before foo: 100; After: 10

IMU: this is very confusing. For some reason it seems like that direct call to destructor was optimized out, i.e. ignored. why?

I tried to appeal to gcc community, but was quite surprised with they reply: gcc-bug-103843

Their inability to understand the case, pushing off and jokes, make the environment contr-productive, I was not ready to continue.

I made some additional investigation:

#include <iostream>

class S {
public:
   static constexpr size_t s_len=10;
public:
   S()  { for(auto i=0; i<s_len; ++i) m_arr[i]=10; }
   ~S() { for(auto i=0; i<s_len; ++i) m_arr[i]=11; /*prnt(); std::cout << std::endl;*/ }
   void prnt() { for(auto i=0; i<s_len; ++i) std::cout << m_arr[i] << "; "; }
public:
   int m_arr[s_len];
};

int main()
{
   S s;
   do {
      std::cout << "Before foo: "; s.prnt();
      s.~S();
      std::cout << "; After: ";    s.prnt();
      std::cout << std::endl;
   } while (false);
   return 0;
}

Running:

$g++ -O0 ./d2.cpp -o d2
$./d2 
Before foo: 10; 10; 10; 10; 10; 10; 10; 10; 10; 10; ; After: 11; 11; 11; 11; 11; 11; 11; 11; 11; 11; 
$g++ -O3 ./d2.cpp -o d2
$./d2 
Before foo: 10; 10; 10; 10; 10; 10; 10; 10; 10; 10; ; After: 10; 10; 10; 10; 10; 10; 10; 10; 10; 10; 

This is even more confusing.

One more test:

#include <iostream>

class S {
public:
   static constexpr size_t s_len=10;
public:
   S()  { for(auto i=0; i<s_len; ++i) m_arr[i]=10; }
   ~S() { for(auto i=0; i<s_len; ++i) m_arr[i]=11; prnt(); std::cout << std::endl; }
   void prnt() { for(auto i=0; i<s_len; ++i) std::cout << m_arr[i] << "; "; }
public:
   int m_arr[s_len];
};

int main()
{
   S s;
   do {
      std::cout << "Before foo: "; s.prnt();
      s.~S();
      std::cout << "; After: ";    s.prnt();
      std::cout << std::endl;
   } while (false);
   return 0;
}

Running:

$g++ -O0 ./d2.cpp -o d2
$./d2 
Before foo: 10; 10; 10; 10; 10; 10; 10; 10; 10; 10; 11; 11; 11; 11; 11; 11; 11; 11; 11; 11; 
; After: 11; 11; 11; 11; 11; 11; 11; 11; 11; 11; 
11; 11; 11; 11; 11; 11; 11; 11; 11; 11; 
$g++ -O3 ./d2.cpp -o d2
$./d2 
Before foo: 10; 10; 10; 10; 10; 10; 10; 10; 10; 10; 11; 11; 11; 11; 11; 11; 11; 11; 11; 11; 
; After: 11; 11; 11; 11; 11; 11; 11; 11; 11; 11; 
11; 11; 11; 11; 11; 11; 11; 11; 11; 11; 

This is perfectly correct.

From what I see the direct call to destructor is not trivial and has some agreements. In same cases (-O3) it behaves differently, in addition it differs from one version compiler to the other.

I would like to better understand what these agreements are? Are they reliable? Who can help?

g++ --version
g++ (GCC) 10.2.1 20210130 (Red Hat 10.2.1-11)

Thnx in advance George

  • 10
    Destroying an object more than once produces undefined behavior. So is using an object after its lifetime has ended. Compilers typically optimize around the assumption our code is well behaved and doesn't produce undefined behavior. The optimization itself is not a bug. And the GCC community tried to tell you that very politely before your insistence likely frustrated them. – StoryTeller - Unslander Monica Dec 28 '21 at 09:01
  • 3
    You are not allowed to read a value that is destroyed. So the compiler saves some work by not writing a value (`i=10;`) that cannot legally be read anyway. – BoP Dec 28 '21 at 09:15
  • I beg your pardon. Why do you talk about validity of my code? It was not a question. I did not asked that. The question was about destructor's agreement. What does it do what does it not do? Do you have knowledge on this? Earning Reputations? – Georgii Shagov Dec 28 '21 at 09:23
  • 1
    [OT]: `do {/*..*/} while(false);` is mostly a trick for MACRO; for regular code, you might just use block `{/*..*/}`. – Jarod42 Dec 28 '21 at 09:31
  • Most likely, when optimization is enabled, the compiler is allocating `s` in a register instead of in memory, and after the destructor is called, the register is used for something else. So trying to access it after the destructor gets strange values. – Chris Dodd Dec 28 '21 at 09:51
  • 4
    `Why do you talk about validity of my code?` Languages like C++ has this notation, that the code itself can be "invalid". For example, `int a[5]; a[100] = 123;` is invalid, like your code is. Research the term _undefined behavior_, in particular https://stackoverflow.com/questions/2397984/undefined-unspecified-and-implementation-defined-behavior . `What does it do what does it not do?` Compilers are only prepared to work with "valid" code in the first place. When you write invalid code and put that to the compiler, no one cares what will happen. Reasoning what happens then is just pointless. – KamilCuk Dec 28 '21 at 10:16
  • 2
    Please learn about "undefined behaviour" and avoid it. Anything can happen when you enter its realm. – MatG Dec 28 '21 at 12:08
  • Calling a destructor explicitly is almost always an error. Even just `int main() { S s; s.~S(); }` produces Undefined Behavior since `s` is destroyed twice (at `s.~S()` and at the end of `main`). – François Andrieux Dec 28 '21 at 16:35

2 Answers2

5

Using an object after it has been destructed is undefined behaviour, therefore the compiler is under no obligation to ensure that your code will work as you expect. It is free to assume that the value of i after the destructor is run doesn't matter so it can make optimisations accordingly, it may even be that the optimiser is simply not written to handle such a case which is why you see a result of 0 which may be some default value. Again this is a completely legal thing for the compiler to do as your code's behaviour is undefined, you're lucky it doesn't just crash.

You can see this is probably what's happening by the fact that your code "works" when optimisations are disabled.

Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
3

Compilers are allowed to assume that Undefined Behavior does not happen, up to assuming that your entire program won't run. In this case, it just assumed the do ... while(false) loop does not run. That too is a a valid assumption.

This is not a matter of opinion. If you're trying to argue that a do...while(false) loop runs once, you're arguing that the statement works as defined by the C++ Standard. But the C++ Standard does not apply at all to programs with Undefined Behavior.

MSalters
  • 173,980
  • 10
  • 155
  • 350