https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127072
Bug ID: 127072
Summary: -Wfree-nonheap-object false positive on
vector::push_back after a guarded pop_back
(14.2/14.3/15.2/15.3, clean in 12.4/16.1)
Product: gcc
Version: 15.3.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: tree-optimization
Assignee: unassigned at gcc dot gnu.org
Reporter: rogerio.souza at gmail dot com
Target Milestone: ---
The case below was create with Claude support. See Also: 115016, 112370, 99098:
testcase.cpp:
#include <vector>
struct H;
const H* const* beg();
const H* const* end();
void f(const H* x)
{
std::vector<const H*> v(beg(), end());
if (!v.empty())
v.pop_back();
v.push_back(x);
}
Build command:
g++ -c -std=c++23 -O2 -Wall testcase.cpp
The program has no undefined behaviour: pop_back() is guarded by !empty(), and
the
only operator delete reachable from push_back() is _M_deallocate(_M_start, ...)
in
_M_realloc_append, i.e. exactly the pointer operator new returned for the
vector's
buffer. No pointer with nonzero offset is ever passed to operator delete.
The guard is what triggers it. After it, the pointer reaching _M_deallocate
merges two
states: the empty-input path (_M_start == nullptr, so push_back reallocates and
_M_deallocate's if (__p) skips the free) and the non-empty path (spare
capacity, so
push_back never reallocates/deallocates at all). The deallocation the warning
points at
is unreachable on both incoming paths; the merged pointer is no longer
recognised as the
allocation base, so it's misreported as an offset pointer.
Removing the if (!v.empty()) guard silences this warning on 14/15 but then
triggers a
-Warray-bounds false positive on the now-unguarded pop_back() on 12.4/14.x/16.1
(clean on 15.x) — see the companion report I'm filing for that. gcc 14.x
rejects both
spellings, so with 14/15/16 in use there is no way to write "drop the last
element, then
append" that stays warning-free everywhere.
Version table (g++ -std=c++23 $opt -Wall, -Werror-worthy build):
g++ -O1 -O2 -O3
12.4.0 clean clean clean
14.2.0 clean warns warns
14.3.0 clean warns warns
15.2.0 clean warns warns
15.3.0 clean warns warns
16.1.0 clean clean clean
Independent of -std (c++17..c++26) and of -fno-exceptions.
v.resize(v.size() - 1) instead of the guarded pop_back() silences the warning
on all
of 14/15/16.
---
### Bug 2 — `-Warray-bounds` / `[[assume]]`
**Component:** tree-optimization
**Version:** 16.1.0 (also reproduces on 12.4.0, 14.2.0, 14.3.0)
**Severity:** normal / diagnostic
**See Also:** 104017, 113239
**Summary:** `[[assume(!v.empty())]]` does not suppress `-Warray-bounds` on
`vector::pop_back` (unlike `if (empty()) std::unreachable()`)
**Description:**
testcase2.cpp:
#include <vector>
struct H;
const H* const* beg();
const H* const* end();
void f(const H* x)
{
std::vector<const H*> v(beg(), end());
v.pop_back();
v.push_back(x);
}
=============================================
$ g++ -c -std=c++23 -O2 -Wall testcase.cpp
.../bits/stl_construct.h:88:9: warning: array subscript -1 is outside array
bounds
of 'const H* [1152921504606846975]' [-Warray-bounds=]
88 | __location->~_Tp();
... inlined from 'void std::vector<T>::pop_back()' ...
... inlined from 'void f(const H*)' at testcase.cpp:10:15
Taken alone this is defensible: `beg()`/`end()` are opaque, so the compiler
can't prove
`v` is non-empty, and `pop_back()` on an empty vector is UB. Two problems:
1. **`[[assume]]` does not suppress it**, even though it supplies exactly the
missing
fact:
```c++
void assumed(const H* x)
{
std::vector<const H*> v(beg(), end());
[[assume(!v.empty())]];
v.pop_back(); // still warns
v.push_back(x);
}
void unreachable(const H* x)
{
std::vector<const H*> v(beg(), end());
if (v.empty())
std::unreachable();
v.pop_back(); // clean
v.push_back(x);
}
=============================================
Only assumed warns; unreachable is clean. So the pass can consume the fact — it
just isn't derived from [[assume]]. [[assume(v.size() > 0)]] behaves the same.
That leaves a -Werror user with no standard C++23 way to state the
precondition:
the portable spelling is ignored and only the std::unreachable() idiom works.
The location is a no-op. _Tp is const H*, trivially destructible, so
__location->~_Tp(); in std::destroy_at generates no code at all. A subscript
diagnostic anchored to a pseudo-destructor call that emits nothing is
confusing, and
'const H* [1152921504606846975]' in the message is a synthesised maximum-size
type,
not anything in the source.
It contradicts a companion diagnostic. Adding if (!v.empty()) around the
pop_back() — the obvious "fix" for this warning — makes gcc 14.2/14.3/15.2/15.3
emit a bogus -Wfree-nonheap-object on the following push_back() (filed
separately). gcc 14.x rejects both spellings, so with 14/15/16 in use there is
no
spelling of "drop the last element, then append" that is warning-free
everywhere.
Version table:
g++ -O1 -O2 -O3
12.4.0 clean warns warns
14.2.0 clean warns warns
14.3.0 clean warns warns
15.2.0 clean clean clean
15.3.0 clean clean clean
16.1.0 clean warns warns
Independent of -std (c++17..c++26). 15.x being clean is what made the migration
to 16 look like a regression.