Home > Enterprise >  Direct Destructor Call Optimized out
Direct Destructor Call Optimized out

Time:12-29

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

CodePudding user response:

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.

CodePudding user response:

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.

CodePudding user response:

My sincere appreciation to all those who commented. It is more or less the same. After a call to destructor, any query to member variables is 'undefined behaviour'. This is clear. And yet, there are some cases,

how about this:

#include <iostream>

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

static S s;

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


As you can see I made the instance of the class - static.

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

Well, this could be annoying. Imagine, If I need to refactor a legacy code and I am moving some classes as static (making library single threaded for instance, or keeping some single-tons, connections, as a sample), then in some place I am calling destructor for these classes Instances. So, according to this undefined behaviour the content of these classes - unpredictable, though they are static. I have no any intention in pushing the case, yet IMU such optimization is questionable.

Regards

  • Related