https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127003
Bug ID: 127003
Summary: LTO fails to materialize defaulted move-assignment
operator that is only referenced from an
exception-cleanup (EH unwind) path, causing "undefined
reference" at final link
Product: gcc
Version: 13.3.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: lto
Assignee: unassigned at gcc dot gnu.org
Reporter: ulrich.telle at gmx dot de
Target Milestone: ---
GCC version: reproduced with GCC 13 (Ubuntu)
gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
and a current GCC version on Fedora 44
gcc (GCC) 16.1.1 20260515 (Red Hat 16.1.1-2)
(originally reported via Fedora's rpmbuild for wxsqlite3-5.0.1); please fill in
exact `gcc --version` output for
both systems when filing.
Reproduction steps (from a clean checkout):
1. Install wxWidgets development packages (e.g. `libwxgtk3.2-dev` on
Ubuntu, or the equivalent wxGTK3 devel package on Fedora).
2. Clone the wxsqlite3 repository:
git clone https://github.com/utelle/wxsqlite3.git
3. Switch to tag v5.0.1:
git checkout v5.0.1
3. cd wxsqlite3 && autoreconf -fi
4. mkdir build-gtk && cd build-gtk
5. ../configure
6. Edit the generated Makefile to add LTO flags to CXXFLAGS/CFLAGS,
e.g.:
-O2 -flto=auto -ffat-lto-objects
(any of -flto=auto, or -flto=auto -ffat-lto-objects, is
sufficient to trigger the bug; -fno-lto avoids it entirely)
7. make
Result:
CXXLD samples/minimal
/usr/bin/ld: /tmp/ccxubXuK.ltrans0.ltrans.o: in function
`testTransaction() [clone .cold]':
samples/minimal.cpp:108:(.text.unlikely+0x34a): undefined
reference to `wxSQLite3::ResultSet::operator=(wxSQLite3::ResultSet&&)'
collect2: error: ld returned 1 exit status
make: *** [Makefile:653: samples/minimal] Error 1
Background:
wxSQLite3::ResultSet declares its special member functions as
explicitly defaulted, inline, in the class body:
ResultSet(const ResultSet&) = default;
ResultSet& operator=(const ResultSet&) = default;
ResultSet(ResultSet&&) = default;
ResultSet& operator=(ResultSet&&) = default;
This pattern was introduced in wxsqlite3 5.0.0 (together with a new
requirement for -std=c++17); prior releases used different,
hand-written special member functions and did not show this problem.
Building the same code without -flto (or with -fno-lto) succeeds.
The failing call site (samples/minimal.cpp, function
testTransaction()) is:
bool exceptionCaught = false;
...
try {
wxSQLite3::Transaction t(db);
db->ExecuteUpdate(wxS("INSERT INTO test (col1) VALUES (3)"));
throw "Abort commit";
}
catch (...) {
exceptionCaught = true;
}
if (exceptionCaught) {
set = db->ExecuteQuery(wxS("SELECT * FROM test")); // <--
move-assignment call, reported as undefined
...
}
Note the move-assignment call is not directly inside the catch
block; it is inside an `if` block guarded by a bool that was set
inside a preceding catch(...) handler. The linker specifically
complains about the `.cold` clone of testTransaction(), i.e. the
part of the function GCC split off as an infrequently-executed
partition (exception-cleanup-adjacent code).
-flto-report / -flto-report-wpa output for this build shows the
program is split into 2 callgraph partitions:
[WPA] # callgraph partitions: 2
and the diagnostic references the resulting ltrans0.ltrans.o for the
.cold clone. This suggests that GCC's LTO WPA stage decides, on a
per-partition basis, whether the (trivial, vague-linkage) defaulted
move-assignment operator needs to be materialized as a standalone
symbol; apparently no partition ends up containing a materialized
definition, even though a real (non-inlined) call to it remains in
the infrequently-executed .cold clone of testTransaction(), which is
compiled/linked as part of a different (or at least separately
processed) ltrans unit.
Attempts to isolate this further were only partially successful:
- Handwriting a small, self-contained reproducer (a class with
defaulted move assignment, called only from a similar try/catch +
flag pattern) did NOT reproduce the failure, even when explicitly
forcing multiple LTO partitions via
`--param lto-partitions=N -flto-partition=balanced`.
- Reducing samples/minimal.cpp itself down to just testTransaction()
and its helper initDB() (removing all other, unrelated sample
functions) also failed to reproduce the issue when built from a
fully clean checkout/configure/make cycle.
- However, building that same reduced testTransaction()+initDB()
file into a build directory that still contained leftover object
files from a prior full build of samples/minimal.cpp (i.e. without
an intervening full rebuild of the rest of the translation unit)
DID reproduce the failure at the same source line.
This points to the overall size/composition of the translation
unit (or of the whole-program callgraph as seen by LTO WPA) as a
relevant factor in whether the bug triggers, not just the local
control-flow shape around the move-assignment call. Because of this,
we were not able to produce a small, wxWidgets-independent minimal
reproducer with confidence; the steps above (full wxsqlite3 sample
build) are the only reliably reproducing case we found, starting
from a clean checkout.
Confirmed workarounds:
- Building without LTO (-fno-lto) avoids the problem entirely.
- Adding an artificial, non-optimizable reference to the address of
the defaulted operator in the same translation unit as the class
definition (forcing GCC to keep a materialized definition) also
resolves the issue:
namespace {
volatile auto force_link =
static_cast<wxSQLite3::ResultSet&(wxSQLite3::ResultSet::*)(wxSQLite3::ResultSet&&)>(
&wxSQLite3::ResultSet::operator=);
}
Ruled out during investigation:
- Not caused by -ffat-lto-objects specifically (plain -flto=auto
alone reproduces it).
- Not caused by -freorder-blocks-and-partition / hot-cold function
splitting specifically (adding
-fno-reorder-blocks-and-partition does not avoid the failure).
- Not a compiler version drift issue (same gcc version throughout
testing) and not a ccache artifact (ccache not installed/used).
- Does not appear to be simple LTO-thread non-determinism (5
consecutive clean rebuilds with identical flags all failed
consistently once a genuinely clean checkout/build was used;
earlier apparent "flakiness" turned out to be caused by stale
configure/build-directory state from previous experiments, not by
the compiler itself).
We would appreciate guidance on how to reliably minimize this
further, or, failing that, would be glad to provide the full
wxsqlite3 source tree / exact build logs on request.