https://gcc.gnu.org/bugzilla/show_bug.cgi?id=128157
Bug ID: 128157
Summary: virtual call in a base destructor through a virtual
base resolved to a more-derived override at -O2
Product: gcc
Version: 17.0
Status: UNCONFIRMED
Keywords: wrong-code
Severity: normal
Priority: P3
Component: ipa
Assignee: unassigned at gcc dot gnu.org
Reporter: yqtian668 at gmail dot com
Target Milestone: ---
Host: x86_64-pc-linux-gnu
Target: x86_64-pc-linux-gnu
Build: x86_64-pc-linux-gnu
Created attachment 65953
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65953&action=edit
Preprocessed testcase (gcc-dtor-vbase-most-derived-override.ii)
At `-O2`, GCC resolves a virtual call made by `D::~D()` to `E::f()`, even
though virtual dispatch
during `D::~D()` must use the final overrider in `D`. The executable prints 5
instead of 4.
## Reproducer
```cpp
#include <cstdio>
struct A {
virtual int f() = 0;
};
struct B {
virtual void g() {}
};
struct C : B, A {};
struct D : virtual C {
~D() { std::printf("%d\n", static_cast<C&>(*this).f()); }
int f() override { return 4; }
};
struct E : D {
int f() override { return 5; }
};
int main() {
E e;
}
```
Command:
```text
g++ -std=c++11 -O2 testcase.cpp -o test
./test
```
**Actual behavior:** The executable prints `5`. GCC resolves the virtual call
in `D::~D()` to the
virtual thunk for `E::f()`.
**Expected behavior:** The executable should print `4`. The conversion from `D`
to its virtual
base `C` is valid while `D` is being destroyed under [class.cdtor]/3. Under
[class.cdtor]/4, a
virtual call on the object under destruction selects the final overrider in the
destructor's
class, not an override in a more-derived class. The call must therefore select
`D::f()`.
https://eel.is/c++draft/class.cdtor#3
https://eel.is/c++draft/class.cdtor#4
GCC prints `4` at `-O0`, `-O1`, and `-Os`. GCC also prints `4` at `-O2` when
`-fno-devirtualize`, `-fno-inline`, `-fno-tree-forwprop`, or
`-fno-strict-aliasing` is added.
Clang trunk prints `4` at `-O2`.
## Regression evidence
GCC 4.9.4 prints `4` at `-O2`. GCC 5.5.0, GCC 10.5.0, GCC 13.3.0, and current
trunk print `5`.
The parent of r5-3634, commit `8e1ba78f1b8eedd6c65c6f0e6d6d09a801de5d3d`, also
prints `5`,
so this regression predates that change. The exact first bad commit has not
been identified.
## Suspected cause
The initial IPA devirtualization analysis records both valid targets:
```text
virtual int D::_ZTvn8_n32_N1D1fEv()
virtual int E::_ZTvn8_n32_N1E1fEv()
```
After inlining `E::~E()` and `D::~D()` into `main`, `forwprop2` asks
`possible_polymorphic_call_targets` about the guarded `OBJ_TYPE_REF`
expressions. Both queries are
resolved to the thunk for `E::f()`. The pass deletes the guarded alternatives
and the residual
virtual call, then leaves `printf ("%d\n", 5)`.
The context at that point uses the outer object `e` of type `E` and is marked
as possibly in
construction or destruction. The likely failure is in the construction-base
target walk: `C` is
a virtual base of `D`, and the walk from the complete `E` object reaches `C`
without retaining
`D::f()` as a possible overrider.
## Compiler version
```text
Using built-in specs.
COLLECT_GCC=/opt/gcc/bin/g++
COLLECT_LTO_WRAPPER=/opt/gcc/libexec/gcc/x86_64-pc-linux-gnu/17.0.0/lto-wrapper
Target: x86_64-pc-linux-gnu
Configured with: /src/configure --prefix=/opt/gcc --enable-languages=c,c++
--disable-multilib --disable-bootstrap --enable-checking=yes
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 17.0.0 20261025 (experimental) (GCC)
```
Verified with GCC trunk commit `cd7a85774a992c4759ebd03a26ce03cbb934e4d9`.