https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127109
Bug ID: 127109
Summary: [C++26] P0963 structured binding as a condition
rejected during constant evaluation when the type uses
the tuple protocol
Product: gcc
Version: 16.2.0
Status: UNCONFIRMED
Keywords: c++26, rejects-valid
Severity: normal
Priority: P3
Component: c++
Assignee: unassigned at gcc dot gnu.org
Reporter: filip.sajdak at gmail dot com
Target Milestone: ---
C++26 P0963R3 allows a structured binding declaration to be the condition of an
if. GCC implements this (PR115745). It works for aggregates, and it works at
run time for types that decompose through the tuple protocol. It is rejected
when all three of the following hold at once:
1. the structured binding declaration is used as a condition (P0963), and
2. the bound type decomposes via the tuple protocol (tuple_size / get<I>())
rather than member-wise, and
3. the call is evaluated in a constant expression.
Removing any one of the three makes GCC accept the program. Clang accepts every
variant.
Reproducer (only <cstddef> and <tuple>):
#include <cstddef>
#include <tuple>
struct Result {
int a;
int b;
bool ok;
constexpr explicit operator bool() const { return ok; }
template <std::size_t I> constexpr int get() const { return I == 0 ? a : b;
}
};
template <> struct std::tuple_size<Result> :
std::integral_constant<std::size_t, 2> {};
template <std::size_t I> struct std::tuple_element<I, Result> { using type =
int; };
constexpr int sum_if_ok(int v) {
if (auto [a, b] = Result{v, v * 2, v != 0}) {
return a + b;
}
return -1;
}
static_assert(sum_if_ok(1) == 3);
static_assert(sum_if_ok(0) == -1);
int main() { return 0; }
Command:
g++ -std=c++26 bug.cpp
No other flags and no third-party headers. -Wall -Wextra produces no additional
diagnostics.
Actual behaviour:
<source>:24:28: error: non-constant condition for static assertion
24 | static_assert(sum_if_ok(1) == 3);
| ~~~~~~~~~~~~~^~~~
<source>:24:24: in 'constexpr' expansion of 'sum_if_ok(1)'
<source>:18:5: error: accessing '<anonymous>' outside its lifetime
18 | if (auto [a, b] = Result{v, v * 2, v != 0}) {
| ^~
<source>:18:46: note: declared here
(The same pair of errors repeats for the second static_assert.)
Expected behaviour:
The program compiles and both assertions hold. The object introduced by the
structured binding declaration should remain within its lifetime for the
evaluation of the condition and the substatement, as it does at run time and as
it does for aggregates.
Versions tested (reproduces on the current release and on today's trunk
snapshot):
gcc version 16.2.0 (Compiler-Explorer-Build-gcc--binutils-2.44)
gcc version 17.0.0 20260826 (experimental)
(Compiler-Explorer-Build-gcc-6f7fd05bd3ba0ba41703589b29f82fa37fd0d9d5-binutils-2.44)
Target: x86_64-linux-gnu
Configured with: ../gcc-16.2.0/configure
--prefix=/opt/compiler-explorer/gcc-build/staging --build=x86_64-linux-gnu
--host=x86_64-linux-gnu --target=x86_64-linux-gnu --disable-bootstrap
--enable-multiarch --with-abi=m64 --with-multilib-list=m32,m64,mx32
--enable-multilib --enable-clocale=gnu
--enable-languages=c,c++,fortran,ada,objc,obj-c++,go,d,m2,rust,cobol,algol68
--enable-ld=yes --enable-gold=yes --enable-libstdcxx-time=yes
--enable-linker-build-id --enable-lto --enable-plugins --enable-threads=posix
--with-pkgversion=Compiler-Explorer-Build-gcc--binutils-2.44
These are Compiler Explorer builds, which is why the pkgversion string is
theirs; the links below reproduce it with no local setup.
Variants tested:
# decomposition form evaluation GCC
16.2/trunk Clang 22.1
1 aggregate (member-wise) P0963 cond constant accepts
accepts
2 tuple protocol P0963 cond run time accepts
accepts
3 tuple protocol separate cond constant accepts
accepts
4 tuple protocol, get() returns int P0963 cond constant REJECTS
accepts
5 tuple protocol, get() returns const& P0963 cond constant REJECTS
accepts
Variant 3 is the workaround: declaring the binding first and writing the
condition separately is accepted everywhere.
Variant 5 is worth separating out. With get() returning int by value the
bindings are references bound to temporaries, so one plausible reading is that
this is about lifetime extension of those temporaries. It is not. Changing
get() to return const int& (with tuple_element<I, Result>::type correspondingly
const int) removes the temporaries entirely, and GCC still rejects it. The
object whose lifetime is reported as ended is the condition variable itself.
Compiler Explorer:
GCC 16.2, rejected: https://godbolt.org/z/MMxhKv7sT
GCC trunk, rejected: https://godbolt.org/z/qdqjcdbxh
Clang 22.1, accepted: https://godbolt.org/z/vK1hsh14r
Possibly relevant: the P0963 implementation (r15-1793) introduced a TARGET_EXPR
to hold the result of the contextual conversion to bool across the get() calls
for tuple-protocol types, stashed in DECL_DECOMP_BASE. That is the code path
specific to the combination that fails here, and the aggregate case does not
exercise it.
How it was found: while checking whether P0963 shortens a compile-time regular
expression example. ctre::regex_results decomposes through the tuple protocol
and CTRE matches are routinely evaluated in constant expressions, so a
constexpr function using the P0963 spelling assembles all three conditions
without anyone aiming for the combination. Any tuple-protocol type with an
operator bool() reaches the same place.
Related: PR115745 (the implementation), PR118833 and PR116113 (fixed ICEs in
the same feature). PR117784 (P2686R4) is about declaring structured bindings
constexpr, which this testcase does not do; variant 3 above shows
tuple-protocol decomposition in a constant expression already works, so the
failure looks specific to the P0963 condition form rather than to P2686.
PR124584 also concerns the lifetime of a tuple-protocol structured binding, but
at run time under co_await.