It's well known that, under certain conditions, the compiler might elide calls to the copy constructor. However, the Standard is clear saying that the compiler only has the freedom to change the runtime behavior (calling or not the copy constructor) but translation is performed as if the copy constructor is called. In particular, the compiler checks whether there's a valid copy constructor to call.
I came across a situation where a destructor call might be elided but compilers differ on whether a valid destructor needs to exist or not.
Here is a complete example showing how this issue might occur and how compilers differ in behavior.
template <typename T>
struct A {
~A() { (void) sizeof(T); }
};
struct B; // defined elsewhere.
struct C {
A<B> x, y;
~C(); // defined in a TU where B is complete.
};
int main() {
C c;
}
When compiling main()
the compiler generates C
's default constructor. This constructor default initializes first x
and then y
. If an exception is thrown during y
construction, then x
must be destroyed. The generated code looks like this:
new ((void*) &this->x) A<B>; // default initializes this->x.
try {
new ((void*) &this->y) A<B>; // default initializes this->y.
}
catch (...) {
(this->x).~A<B>(); // destroys this->x.
throw;
}
Knowing that A<B>
's default constructor is trivial (and doesn't throw), under the as-if rule, the compiler might simplify the code to:
new ((void*) &this->x) A<B>; // default initializes this->x.
new ((void*) &this->y) A<B>; // default initializes this->y.
Therefore, there's no need to call ~A<B>()
. (Actually, the compiler can even remove the two initializations above since the A<B>
's constructor is trivial, but this is not important to this discussion.)
The question is: Even though the call to the destructor might be elided, should the compiler verify whether a valid destructor is available? I couldn't find anything on the Standard that clarifies the matter. Can anyone provide relevant quotes?
If the compiler decides to not translate ~A<B>()
(like gcc and Visual Studio do) then the compilation succeeds.
However, if the compiler decides to translate ~A<B>()
anyway (like clang and icc do), then it raises an error because here B
is an incomplete type and its size cannot be taken.
I don't think this is specified by the standard. If ~A<B>
is instantiated then it is ill-formed and a diagnostic is required. And as you say if constructing y
throws, then x
must be destroyed.
However, constructing y
can never throw, so arguably there will never be a requirement for the destructor's definition to exist (15.2/2, 14.7.1/3).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With