4

Related: How does =delete on destructor prevent stack allocation?

First, I realize this sort of thing is playing with fire, tempting UB. I'm asking to get a better understanding of corner-cases of unions.

As I understand it, if I have a class with a c'tor and d'tor and put it in a union, the compiler will force me to tell it what to do about construction and destruction. As in

#include <iostream>

struct S {
    int x;
    S() : x(-1) { std::cout << "S()" << std::endl; }
    ~S() { std::cout << "~S()" << std::endl; }
    static S& instance() {
        union DoNotDestruct {
            S value;
            DoNotDestruct() : value() {}
            ~DoNotDestruct() {}
        };
        static DoNotDestruct inst;
        return inst.value;
    }
};

int main() {
    return S::instance().x;
}

which just reports construction, not destruction.

However, it appears that if I have a deleted destructor, that doesn't work; I have to use placement-new:

#include <new>

// Placement-new version:
struct S0 {
    S0() {}
    ~S0() = delete;
    static S0& instance() {
        union DoNotDestruct {
            S0 value;
            DoNotDestruct() {
                // I believe value isn't constructed yet.
                new (&value) S0; // Placement-new.
            }
            ~DoNotDestruct() {} // Omit S0's d'tor.
        };
        static DoNotDestruct inst;
        return inst.value;
    }
};

// Init version:
struct S1 {
    S1() {}
    ~S1() = delete;
    static S1& instance() {
        union DoNotDestruct {
            S1 value;
            DoNotDestruct() : value{} {} // Why does this line want S1::~S1() to exist?
            ~DoNotDestruct() {} // Omit S1's d'tor.
        };
        static DoNotDestruct inst;
        return inst.value;
    }
};

https://godbolt.org/z/7r4ebszor

Here, S0 is happy, but S1 complains on the constructor line that S1::~S1() is deleted. Why?

I tried adding noexcept on the thought that in a class, if you have multiple members, the 0th member needs to be destructible if any subsequent c'tors throw. It looks like even a class with a deleted d'tor can't itself hold an indestructible member even if the c'tors are all noexcept. It gives the same error as the union: https://godbolt.org/z/jx3W1YEPf

Ben
  • 9,184
  • 1
  • 43
  • 56
  • 2
    Notice that clang accepts the code [Demo](https://godbolt.org/z/YceW8G9PM). – Jarod42 Jan 19 '22 at 16:57
  • 1
    I think rationale for `~S1` to be accessible is in case body of `DoNotDestruct` constructor throws (then value should be destructed). – Jarod42 Jan 19 '22 at 16:58
  • @Jarod42 Right. I updated the question with that. I'm a little surprised that a struct with a deleted d'tor apparently can't contain a single element that is a struct with a `noexcept` c'tor and a deleted d'tor. (Clang doesn't like that either: https://godbolt.org/z/G3vrT7c1v) – Ben Jan 19 '22 at 17:00
  • It is somehow the initializer `: value{}` – Sebastian Jan 19 '22 at 17:01

1 Answers1

3

[class.dtor]/15 says that a program is ill-formed if a potentially invoked destructor is defined as deleted.

[class.base.init]/12 says that a destructor of a potentially constructed subobject of a class is potentially invoked in a non-delegating constructor.

[special]/7 says that all non-static data members are potentially constructed subobjects, although the sentence is qualified a bit weirdly I think ("For a class, [...]").

I don't see any exceptions in this for single-member classes, unions or noexcept. So it seems to me that GCC is correct for S1.

Clang is also the only compiler out of Clang, GCC, ICC and MSVC that doesn't reject this version.

For S0 however, this reasoning would also apply, making it ill-formed as well. However none of the four compilers agrees on that, so maybe I am wrong.


The quoted wording comes mostly from CWG issue 1424. Clang lists its implementation status currently as unknown (https://clang.llvm.org/cxx_dr_status.html), as does GCC (https://gcc.gnu.org/projects/cxx-dr-status.html).

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • Thanks! I was under the impression that because `union` lets you decide if and when to construct and destruct members, it was a valid way to make an instance that is never destructed (and seems cleaner than `std::launder` and `std::aligned_storage`. (a) Is that correct? (b) If so, is it really just the deletedness of the d'tor that's the problem: the d'tor is never actually called, right? – Ben Jan 19 '22 at 18:29
  • 1
    @Ben As long as your constructor doesn't throw an exception, there shouldn't be a call to the destructor of `value`. But it cannot be deleted even if you don't throw for sure, presumably so that the compilers don't have to check that condition, I'd guess. Maybe the special case of unions was overlooked or that weird wording is supposed to relate to that(?). – user17732522 Jan 19 '22 at 18:38