38

Please consider the simple example as follows, where the function bar returns an object of class A with private destructor, and mandatory return value optimization (RVO) must take place:

class A { ~A() = default; };
A bar() { return {}; }

The code is accepted by Clang, but rejected by GCC with the error:

error: 'constexpr A::~A()' is private within this context
    2 | A bar() { return {}; }
      |                   ^

https://gcc.godbolt.org/z/q6c33absK

Which one of the compilers is right here?

cigien
  • 57,834
  • 11
  • 73
  • 112
Fedor
  • 17,146
  • 13
  • 40
  • 131

2 Answers2

48

This is CWG 2426. The destructor is potentially invoked within this context, because even after the initialization of the return A object, it's still possible that the function fails to complete successfully: any temporaries created during the return statement, and automatic local variables that are in scope, must be destroyed, and if the destruction throws, then as part of stack unwinding, the A object is destroyed. Compilers should require the destructor to be accessible at this point.

Note 1: exceptions thrown by the destructors of local variables in the outermost scope of the function can be caught by a function try block.

Note 2: after the return object is destroyed, the handler is allowed to execute another return statement. There is an example of this in the standard.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • 3
    Within **this specific context** (the OPs example), it is *trivially proven* that the destructor isn't ever called. Other examples are not so nice. – Deduplicator Aug 05 '21 at 15:06
  • Regarding "Note 1": won't an exception thrown by a destructor, cause a call to `std::terminate"? – Lorah Attkins Aug 05 '21 at 15:36
  • 7
    @LorahAttkins Only if done during stack-unwinding due to another exception. Does not apply here. – Deduplicator Aug 05 '21 at 15:43
  • @Deduplicator That's not the way the standard "works" for many aspects it covers. Dependent on enabled/disabled exception handling and the optimization level for instance, you can find dozends of scenarios where a lot of standard requirements might not be relevant for your specific code portions. For this aspect here further on, it would be almost impossible to output reasonable compiler diagnostics in doubt, why the public availablity of the destructor is required for a specific code sample suddenly. – Secundi Sep 22 '21 at 12:45
  • @Secundi I'm not sure which of my comments you meant. Regarding the first: There is no way this specific function fails after creating the return-value, which is the only thing it does. Thus, the dtor will never be invoked. Why it is still per-definition *potentially invoked* is the interesting question. Regarding the second: That should be straight-forward enough. As an aside, disabled exceptions means non-standard, and also obviously means the trigger in my second comment cannot apply. – Deduplicator Sep 22 '21 at 13:02
  • @Deduplicator yes, the first one. If you want this to be included within the standard, then how to formalize? "If it's trivially proven by the compiler, that the destructor is effectively never used (i.e. assembler output analysis in doubt...), then you don't need it" ? Sounds quite spongy. – Secundi Sep 22 '21 at 13:08
  • @Secundi I never said I wanted to change that part of the standard. I just criticized this answer as it asserts that the dtor might actually be invoked, even though as you also see it is trivially proven impossible. – Deduplicator Sep 22 '21 at 13:33
  • This is just an opinion, @Secundi, but I would say it's _potentially invoked_ because the possibility of invoking still exists. Even if this option will provably never be taken, requiring the compiler to ignore this proof and assume it might be taken greatly simplifies compilation, on the grounds that it means the compiler isn't required to spend time and processing power analysing all similar functions to determine whether or not they will invoke. – Justin Time - Reinstate Monica Oct 06 '21 at 23:25
15

There are many simple cases like in the question where it can be easily proven that the destructor will never be used, however the code is used.

It can get arbitrarily complex to decide that question though, which is the bane of standardization. And leaving it in the hands of the implementers will thus splinter the language, creating incompatible sub-dialects as they go to varying amounts of effort to decide (different) corner-cases.

But even that isn't the end of this, as solving the problem means solving the halting-problem, and thus isn't even intractable, but undecidable.

Thus, side-stepping it as CWG 2426 does is not only for sanity (specifying all the details gets unwieldy extremely fast), but the only choice without capriciously drawing a line after dictating any number of arbitrarily chosen simple cases.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
  • +1 For mentioning the halting problem! I think the core issue here in terms of formalization for the committee was: If the destructor's public availability is is conceivably required, in doubt depending on optimization level and enabled/disabled exception handling further on, the class design itself would depend on these aspects, not to mention the horrific compiler diagnostics in doubt. – Secundi Sep 22 '21 at 12:30
  • This simply isn't the case. Before CWG2426, the wording added by [CWG2176](https://wg21.link/cwg2176) would be what determines if the destructor could potentially be invoked, and no extra wording is necessary. It would only be when anything destroyed after returning (temporaries in the return statement + automatic variables) has a type with a `noexcept(false)` destructor, which the compiler has to enumerate anyways when compiling what destructors need to be called after the return statement – Artyer Apr 07 '22 at 19:47