https://gcc.gnu.org/bugzilla/show_bug.cgi?id=125285

            Bug ID: 125285
           Summary: [16 Regression] -Warray-bounds false positive on PMF
                    dispatch through a 1-byte predicate at -O3
           Product: gcc
           Version: 16.1.1
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: c++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: andrew.teylu at vector dot com
  Target Milestone: ---

Created attachment 64444
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=64444&action=edit
repro.cpp

================================================================================
Description
================================================================================

GCC 16 emits a -Warray-bounds= error at -O3 on a pointer-to-member-function
dispatch sequence whose only possible runtime path is non-virtual. Clean on
every GCC version from 8.3.0 through 15.2.1; fails in 16.1.0 and 16.1.1.
`-flto=8` cures it on 16.x.

Why it's a false positive (short form — full argument is in repro.cpp's
header comment):

  The PMF type at the dispatch site is `bool (Pred::*)(char) const`. Pred
  has no virtual functions, so no PMF of this type can refer to a virtual
  member. The 8-byte vtable load GCC flags only occurs on the virtual
  branch of the Itanium PMF dispatch sequence, which is unreachable for
  any value of this PMF type. The load never executes at runtime — and
  is, in fact, fully dead-code-eliminated by the time codegen runs (the
  same back-end that fires the warning then emits asm without the load).

repro.cpp documents three independent reasons this is a false positive
(type-system argument, final codegen inspection, layout perturbation).
A fourth, complementary check is the dedicated sanitizer witness file:
repro_runnable.cpp adds bodies to the otherwise decl-only functions in
repro.cpp, exercises the same dispatch sequence five times, and exits
clean under `-fsanitize=address,undefined` at both -O0 and -O3. The
-O0 run is the stronger sanitizer evidence: there the generated code
still goes through the generic Itanium PMF dispatch sequence, including
the virtual/non-virtual branch split. If GCC's reported out-of-bounds
load were real, ASan would catch it on the first iteration.

Originally tripped in production by

    std::copy_if(begin, end, back_inserter(v),
                 boost::bind(&Type::operator(), &predicate_obj, ..., _1))

using Boost 1.78, where `Type` is an empty 1-byte class. Boost 1.89 rewrote
_mfi::mem_fn with a single variadic SFINAE-dispatched template and
incidentally avoids this trigger; downstream codebases pinned to Boost 1.78
(or any release with the older per-arity cmfN structure) are blocked at
-Werror.

================================================================================
Diagnostic
================================================================================

    repro.cpp: In member function 'void cmf3::operator()(Pred*, char) const',
        inlined from 'void list4::operator()(cmf3&, A) [with A = rrlist1]'
            at repro.cpp:128:6,
        inlined from 'void bind_t::operator()(char)' at repro.cpp:148:7,
        inlined from 'void my_copy_if(char*, char*, OutIt, bind_t)
            [with OutIt = BackInserter]' at repro.cpp:162:22,
        inlined from 'void drive()' at repro.cpp:171:13:
    repro.cpp:117:13: error: array subscript 'int (**)(...)[0]' is partly
                      outside array bounds of 'Pred [1]'
[-Werror=array-bounds=]
      117 |     (u->*f_)(b3);
          |     ~~~~~~~~^~~~
    repro.cpp:126:10: note: object 'extra' of size 1

================================================================================
Steps to reproduce
================================================================================

    g++ -std=c++17 -O3 -Wall -Wextra -Werror -c repro.cpp -o repro.o

    (exit 1 on 16.x, exit 0 on 15.2.1 and earlier)

================================================================================
Cross-version matrix
================================================================================

Same repro, identical flags:

    g++ 8.3.0   -O3                       : clean
    g++ 11.5.0  -O3                       : clean
    g++ 12.5.0  -O3                       : clean
    g++ 13.4.0  -O3                       : clean
    g++ 14.3.0  -O3                       : clean
    g++ 15.2.1  -O3                       : clean       (2025-10-06)
    g++ 16.1.0  -O3                       : FAIL        (this bug)
    g++ 16.1.1  -O3                       : FAIL        (2026-05-08)
    g++ 16.1.1  -O0 / -O1 / -O2 / -Og     : clean
    g++ 16.1.x  -O3 -flto=8               : clean

Regression introduced between 15.2.1 (2025-10-06) and 16.1.0; have not
git-bisected within that window. The 15.2.1 and 16.1.1 builds tested are
openSUSE Tumbleweed packages; behaviour matches an independently-built
BUILD64 16.1.0.

================================================================================
Workarounds
================================================================================

    - Build the affected TU at `-O2` or below (clean on 16.1.1 at
      `-O0`, `-O1`, `-O2`, and `-Og`)
    - `-flto=8` (cures on 16.x; defers analysis past devirtualization)
    - `-Wno-error=array-bounds` on the affected TU
    - `-fno-inline` (also cures on 16.1.1 by breaking the inlining chain
      that exposes the warning)
    - Source: replace `boost::bind(&Type::op, ..., _1)` with a lambda
    - Source: pad the predicate type to sizeof(void*)
    - Upgrade Boost to 1.89+

================================================================================
Reduction provenance
================================================================================

Started from a real production site using boost::bind on an empty 1-byte
predicate, fed to std::copy_if with a back_insert_iterator. Preprocessed
against Boost 1.78 (-isystem $BOOST/include -O3 -E), reduced with
cvise 2.12.0 against an interestingness test requiring GCC 12.x clean +
GCC 16.x array-bounds fail. Manual cleanup afterwards to make the chain
readable while preserving the trigger; load-bearing oddities (e.g.
declared-but-undefined `rrlist1::operator[]`) are documented inline in
repro.cpp.

Reply via email to