C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Function only accepts parameter that will persist

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Mon, 6 May 2024 15:43:52 -0400
On Mon, May 6, 2024 at 2:46 PM Frederick Virchanza Gotham via Std-Proposals
<std-proposals_at_[hidden]> wrote:

> On Mon, May 6, 2024 at 7:15 PM Thiago Macieira wrote:
> > On Monday 6 May 2024 10:19:23 GMT-7 Frederick Virchanza Gotham via Std-
> >
> > Why would it ever need to mandate that? Why should EVERY SINGLE constexpr
> > function have to have a consteval-only portion?
>
>
> What I meant was:
> For a constexpr function that contains consteval-only portions,
> said function must behave exactly as though you had copy-pasted all of
> those consteval-only portions out into a separate "consteval"
> function.
>
> So the following function:
>
> constexpr bool Func(int a, int const b)
> {
> a += b;
> if consteval { ++a; }
> a -= 3;
> }
>
> when invoked in a constant-evaluated context, must behave exactly the same
> as:
>
> consteval bool Func(int a, int const b)
> {
> a += b;
> ++a;
> a -= 3;
> }
>

Right. (I think Thiago misread your email: "I think all constexpr functions
[*that are*] like [X] should behave like [Y]" doesn't mean the same thing
as "I think all constexpr functions *should be* like [X] and behave like
[Y].")
However, I'm 99.9% sure that `Func` does already behave exactly the way you
want: whenever it's evaluated in a constant-evaluated context, it *does* do
the `if consteval` part.
The thing you've been concerned about in this subthread (from what little
I've been watching it) is more about *under what circumstances it is
guaranteed* that a context *is* constant-evaluated.
For example,
  constexpr int x = Func(1, 2);
is certainly required to constant-evaluate `Func`.
  const int x = Func(1, 2);
is at least *encouraged* [and in fact it is required] to *attempt* to
constant-evaluate `Func`, although of course that constant-evaluation
attempt might fail and then it would have to retry as a non-constant
expression. I wasn't sure if it was required to make the attempt. But
cppreference tells me that it is. These two places:
https://en.cppreference.com/w/cpp/language/constant_expression#Manifestly_constant-evaluated_expressions
https://en.cppreference.com/w/cpp/language/constant_initialization
seem to answer a lot of questions.

Meanwhile, the original subject of the top-level thread is about "how do I
know if I've got a pointer to ROM/.rodata or a pointer to plain old
RAM/.data (that might change)?" There is no way to know this in C++.
"Rodata-ness" would be an obvious candidate for custom type-qualifiers á la
the CQual project. But it would require tight coupling between the
backend/codegen and the front-end, so it wouldn't be a great fit for a
modern general-purpose compiler. An embedded-systems compiler like Green
Hills would be a more likely place to find such a qualifier — and in fact
such a qualifier might already exist, I don't know.
[EDIT: yup, a `rom` qualifier with almost exactly these semantics exists in
the MPLAB C18
<https://ww1.microchip.com/downloads/en/DeviceDoc/C18_UG__51288e.pdf>
compiler for PIC18. But I'm not sure whether it's automatically applied to
string literals.]
It would look something like this:

// using the fantasy type-qualifier `__rom`
void f(__rom const char *) { puts("rom"); }
void f(const char *) { puts("const"); }
void f(char *) { puts("nothing"); }
char ghello[] = "hello";
const char gworld[] = "world";
void test() {
  const char world[] = "world";
  f("hello"); // prints "rom"
  f(world); // prints "const"
  f(ghello); // prints "nothing"
  f(gworld); // prints "rom"
}
int main() {
  test();
}

Another problem with this idea, for Standard C++ (but not for an individual
vendor like MPLAB who can just accept this tradeoff) is that if
`f("hello")` is supposed to call `f(__rom const char*)`, then
`decltype("hello")` must be `__rom const char[6]` instead of plain `const
char[6]`... and that's going to break a lot of existing metaprogrammers.


Most of your suggestions in this area have been utterly wrong (as far as
anything to do with WG21 is concerned) because you're conflating "const"
with "static storage duration" and (even more importantly) with "lifetime."
The WG21-ification of "Does this object live in .rodata?" is not "Does this
object have static storage duration?"; it's "Does this object have global
*lifetime*?"
https://quuxplusone.github.io/blog/2019/03/11/value-category-is-not-lifetime/
Not only is value category not the same thing as lifetime, storage duration
*also* is not the same thing as lifetime.
For a concrete and recent example: P2447
<https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2447r6.html>
permits static *storage duration* for the backing array of an
initializer_list, but it doesn't change the backing array's *lifetime* at
all.

    void g(const char *s) { return s; }
    void g(std::initializer_list<char> il) { return il.data(); }
    int main() {
        const char *p1 = g("hello");
        assert(*p1 == 'h'); // OK, *p1 is within its lifetime; string
literals have global lifetime
        const char *p2 = g({'h', 'e', 'l', 'l', 'o', '\0'});
        assert(*p2 == 'h'); // UB, *p2 is outside its lifetime
    }

Here `*p2` is outside its lifetime, despite probably being placed in static
storage, because it points into il's backing array, and il's backing array
has the same lifetime as il (i.e. local to `g`).
(I'd welcome a proposal to change that, carefully; but P2447 specifically
didn't try, because that would be scope creep. And this particular UB is
totally irrelevant to the working programmer AFAIC, because of course the
`assert` above *will* pass, on a sufficiently modern compiler, because the
bytes-in-memory are correct, despite its being UB on paper.)

HTH,
Arthur

Received on 2024-05-06 19:44:08