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.

Reply via email to