0

I have found following C++ question (cppQuiz #323):

According to the C++17 standard, what is the output of this program?

#include <iostream>
#include <stdexcept>

struct A {
    A(char c) : c_(c) {}
    ~A() { std::cout << c_; }
    char c_;
};

struct Y { ~Y() noexcept(false) { throw std::runtime_error(""); } };

A f() {
    try {
        A a('a');
        Y y;
        A b('b');
        return {'c'};
    } catch (...) {
    }
    return {'d'};
}

int main()
{
    f();
}

The expected answer for the question is "bcad" with a reference to [except.ctor] in the recent standard. However, according to godbolt, clang 16.0 returns "bad" and gcc 13.1 returns "bacd".

Do anyone know what happens?

Marek R
  • 32,568
  • 6
  • 55
  • 140
user3550394
  • 713
  • 2
  • 6
  • 7
  • Start using a debugger [What is a debugger and how can it help me diagnose problems?](https://stackoverflow.com/questions/25385173/what-is-a-debugger-and-how-can-it-help-me-diagnose-problems) Or maybe the quiz is wrong. – Jason May 29 '23 at 13:27
  • 1
    I'd expect the output to be `bad` or `bacd`, depending whether the `{'c'}` is *materialized* or not. – Eljay May 29 '23 at 13:29
  • 2
    @Jason, I would rather know why the compilers return different bytecode. I understand what each of the compilers did (as I already said I used godbolt) but I don't understand if it is actually a bug in either compiler or implementation-specific behavior by the standard. – user3550394 May 29 '23 at 13:33
  • 1
    @user3550394 I see so you should add the `language-lawyer` tag to your question. Otherwise people will start copy/pasting quiz questions here without trying to understand the problem. – Jason May 29 '23 at 13:33
  • Added some additional output looks like `c` is fully constructed but not destroyed with clang and out of sequence with gcc - link - https://godbolt.org/z/raP4v3Ecf - can someone see what I have missed ? – Richard Critten May 29 '23 at 14:23
  • @RichardCritten Isn't that the case resolved by the defect report mentioned in the answer? – Daniel Langr May 29 '23 at 14:26
  • @ÖöTiib Well, answer given clarifies ;) Dropped my comments anyway, all given in the answer already. – Aconcagua May 29 '23 at 14:46
  • Since C++11 `noexcept` is implicitly added to all destructors. So if exception reaches a destructor terminate is called. Before C++11 terminate is called when exception is thrown in destructor invoked during of unwinding stack to handle other exception (there can't be two exceptions in flight reaching same `catch` statement). – Marek R May 29 '23 at 15:28

1 Answers1

4

The output ought to be bcad.

When the first return statement is reached, the result object of the call to f, which is the temporary object materialized in main from the discarded-value expression f(), will be initialized before any local variables or temporaries are destroyed. See [stmt.return]/3.

There are no temporary objects in the function itself that would be destroyed.

So then local variables are destroyed in reverse order of initialization.

First b is destroyed, outputting b.

Then y is destroyed and throws the exception. The exception is caught by the catch handler and therefore stack unwinding will happen.

Stack unwinding will destroy objects of automatic storage duration in the scope, as well as the already constructed result object, in reverse order of their construction, as long as they haven't been destroyed yet. See [except.ctor]/2 which actually has a very similar example and was clarified by CWG 2176.

b and y have already been destroyed. So only the result object and a remain. The result object was constructed after a.

Stack unwinding will therefore start by destroying the already constructed result object, outputting c.

Then a is destroyed, outputting a, and stack unwinding concludes.

After executing the catch handler, the final return statement is reached, which once again constructs the result object, which in turn is destroyed at the end of the full-expression f() in main to output d.


The compiler seem to not have implemented the defect report for CWG 2176 yet and before that it wasn't clear what the behavior should be.

As for why they haven't implemented it yet, I would guess that this is such an unusual scenario, that they would consider it low priority. Having a throwing destructor is unusual and as the question shows, it is unlikely that the user would have the same expectation on the order of destruction as the standard does. However, I think that Clang's behavior of completely skipping the destruction of the already constructed result object is bit problematic.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • Not sure if it's actually that unlikely, I have had (solely that I missed `b` being destructed first...), forming 25% of those having given a statement – and just four voters is not representative anyway ;) – Aconcagua May 29 '23 at 14:40
  • @Aconcagua I didn't mean to refer to the commentators on the question specifically, but that this is on cppQuiz as a max-difficulty level question in the first place. – user17732522 May 29 '23 at 14:55
  • That's a convoluted example. Good analysis. – Martin York May 29 '23 at 15:41