3

I was reading subsection 18.2 Constructors and Destructors of section 18 exception handling in C++17 standard draft where there is an example(page 384). I tried to understand where destruction of object A returned inside the try block happens but couldn't find. So I copied the example, added some prints and see that dtor of that object is never called. What am I missing here? Someone please explain what's happening here.

#include <stdio.h>

struct A {
    int a_;
    explicit A(int a):a_(a) {
        printf("A(%d)'s ctor\n", a_);
    }
    ~A() {
        printf("A(%d)'s dtor\n", a_);
    }
};

struct Y { ~Y() noexcept(false) {
    printf("y's dtor\n");
    throw 0; } };

A f() {
    try {
        A a(23);
        Y y;
        A b(56);
        return A(100); // #1 who destructs this ??
    } catch (...) {
        printf("handling exception..\n");
    }
    printf("At line %d now..\n", __LINE__);
    return A(200); // #2
}

int main() {
    auto ret = f();
    printf("f returned A(%d) object\n", ret.a_);
    return 0;
}

Above code outputs following:

A(23)'s ctor                                                                                                                                                  
A(56)'s ctor                                                                                                                                                  
A(100)'s ctor                                                                                                                                                 
A(56)'s dtor                                                                                                                                                  
y's dtor                                                                                                                                                      
A(23)'s dtor                                                                                                                                                  
handling exception..                                                                                                                                          
At line 34 now..                                                                                                                                              
A(200)'s ctor                                                                                                                                                 
f returned A(200) object                                                                                                                                      
A(200)'s dtor                                                                                                                                                 


...Program finished with exit code 0
Vishal Sahu
  • 650
  • 12
  • 23
  • 2
    You've demonstrated why destructors can never safely throw exceptions. – Chris Dodd Apr 20 '20 at 02:24
  • The document you link is a post-c++17 draft (see N4659 for the C++17 standard) – M.M Apr 20 '20 at 02:27
  • @ChrisDodd Not really, this program is meant to be well-defined – M.M Apr 20 '20 at 02:27
  • The problem is that any exception specification on a destructor that allows any exceptions is impossible to safely implement, as the exception might be thrown when exiting a frame before that frame is fully destroyed. – Chris Dodd Apr 20 '20 at 02:35
  • @ChrisDodd the standard makes no mention of "frames" – M.M Apr 20 '20 at 02:37
  • [frame](https://en.wikipedia.org/wiki/Call_stack#STACK-FRAME) is a common implementation term – Chris Dodd Apr 20 '20 at 02:47

2 Answers2

4

According to the C++17 standard [except.ctor]/2:

If an exception is thrown during the destruction of temporaries or local variables for a return statement, the destructor for the returned object (if any) is also invoked. The objects are destroyed in the reverse order of the completion of their construction.

Then there is an illustrative example (which you modified slightly) to demonstrate: after the Y destructor throws, the next thing that happens is that the A(100) object should be destroyed, so you should see a destruction message.

The posted output indicates a compiler bug. This is very similar to the issue reported as LLVM bug 12286 and gcc bug 33799.

The latter is marked as being fixed in GCC 10 (after initially being reported in 2007!). However, testing with the version 10.0.1 20200418 (experimental) on Godbolt: even though the test case in the 33799 bug report is fixed, the code in this question remains unfixed. I added a comment to that bug report about this example.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • [§14.3](https://isocpp.org/files/papers/N4860.pdf), the example is very similar to the question. – Tony Tannous Apr 20 '20 at 02:39
  • @TonyTannous yes, I am supposing that the OP looked at that example and added some output debugging – M.M Apr 20 '20 at 02:40
  • This seems like a bad problem in the standard -- implementing this requires major overhead in every frame where there is an object that might throw an exception (whcih I guess is not too bad, as no real program will ever declare a destructor noexcept(false)) – Chris Dodd Apr 20 '20 at 02:45
  • @ChrisDodd if it's intentional by compiler vendors to contradict the standard, then there ought to be a discussion thread on their bug trackers somewhere (although searching those things can be a nightmare) – M.M Apr 20 '20 at 02:47
0

Generally, the caller of a function is responsible for destroying the function's return value, but the precise details are part of the platform C++ ABI.

On most ABIs, calling a function that returns a non-trivial value (anything that doesn't safely fit in a register or perhaps two), is done by passing a hidden extra argument specifying where the return value should be constructed. The caller allocates space in it's frame for the return value and passes a pointer to that space.

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
  • If I understood correctly, caller(`main` here) exited and `ret` got destructed as indicated by line `A(200)'s dtor`. Somewhere before it, I was expecting to see line `A(100)'s dtor` which is puzzling me. Probably, I need to understand nuances and other details of case where exception is thrown in destructors as you mentioned ! – Vishal Sahu Apr 20 '20 at 02:40
  • @VishalSahu the standard explicitly say At `A(100)`, the returned object of type A is constructed. Then, the local variable b is destroyed (8.7). Next, the local variable y is destroyed, causing stack unwinding, resulting in the destruction of the returned object, followed by the destruction of the local variable a – Tony Tannous Apr 20 '20 at 02:52