https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127296
Bug ID: 127296
Summary: [c++26][contracts] a coroutine's postcondition may
odr-use a const by-value parameter, which is
ill-formed
Product: gcc
Version: 16.2.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: c++
Assignee: unassigned at gcc dot gnu.org
Reporter: berne at notadragon dot com
Target Milestone: ---
Created attachment 65541
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65541&action=edit
The accepted const by-value parameter, with four controls
An odr-use of a non-reference parameter in a coroutine's postcondition is
ill-formed. GCC accepts it whenever the parameter is written const.
[dcl.fct.def.coroutine] says the consequence outright, in a note:
"An odr-use of a non-reference parameter in a postcondition assertion of a
coroutine is ill-formed."
The two rules that produce it are [dcl.contract.func], which requires a
parameter a postcondition odr-uses to be const, and
[dcl.fct.def.coroutine]/5, which says the coroutine's definition behaves
as if its parameters have no top-level cv-qualifiers. Not having
cv qualifiers makes it impossible for a coroutine definition's parameters
to be const, and thus they are invalid if they are non-reference parameters
used from a postcondition.
```
#include <coroutine>
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() { }
void unhandled_exception() { }
};
};
// THE BUG: accepted; ill-formed.
Task by_value(const int x) post(x > 0) { co_return; }
// CONTROL: the non-const spelling is caught, by the ordinary const rule
// rather than by anything coroutine-aware. That is what says the coroutine
// restriction itself is missing rather than merely mis-worded.
Task non_const(int x) post(x > 0) { co_return; }
// CONTROL: a reference parameter is fine -- [dcl.fct.def.coroutine]/5 binds
// its frame copy to the same object -- and must keep compiling.
Task by_ref(const int& x) post(x > 0) { co_return; }
// CONTROL: a precondition may name a by-value parameter; the const rule, and
// so this restriction, are postcondition rules.
Task in_pre(int x) pre(x > 0) { co_return; }
// CONTROL: a non-coroutine with the same signature is unaffected.
int not_a_coroutine(const int x) post(x > 0) { return x; }
```
```
$ ./gcc-16.2.0/bin/g++ -std=c++26 -fsyntax-only \
coroutine-postcondition-const-param.cpp
coroutine-postcondition-const-param.cpp:32:28: error: a value parameter used in
a postcondition must be const
32 | Task non_const(int x) post(x > 0) { co_return; }
| ^
coroutine-postcondition-const-param.cpp:32:20: note: parameter declared here
32 | Task non_const(int x) post(x > 0) { co_return; }
| ~~~~^
```
Only non_const is diagnosed, and that is the ordinary const rule firing, not
anything coroutine-aware. by_value -- the same function with the parameter
written const to satisfy that rule -- is accepted silently.
spelling result
Task f(int x) post(x > 0) rejected (const rule)
Task f(const int x) post(x > 0) accepted, should not be
So the coroutine restriction is missing outright rather than mis-worded, and
the const rule was masking its absence: every spelling a user would try
first happens to be caught by the other rule.
DISCOVERY
Found by an audit comparing every test in a C++26 contracts implementation's
own suite against stock trunk, looking for cases that pass locally only
because they were fixed locally.
ANALYSIS
The const rule and the coroutine rule are checked independently, and only
the first is implemented. Because the const rule rejects the non-const
spelling, the coroutine case is only reachable through the const one, which
nothing examines -- so the gap is invisible to any test that does not
deliberately write the parameter const.
VERSIONS -- all on x86_64-linux-gnu
source version accepts by_value
compiler-explorer 16.1.0 yes
compiler-explorer 16.2.0 yes
compiler-explorer 17.0.0 20260909, 919c0d16c91 yes
local build -g 17.0.0 20260909, 7dab38c9d71 yes
```
$ ./gcc-16.2.0/bin/g++ -v
Using built-in specs.
COLLECT_GCC=./gcc-16.2.0/bin/g++
COLLECT_LTO_WRAPPER=/home/jberne4/repos/compilers/gcc-16.2.0/bin/../libexec/gcc/x86_64-linux-gnu/16.2.0/lto-wrapper
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
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 16.2.0 (Compiler-Explorer-Build-gcc--binutils-2.44)
```