Obvious Things C Should Do

Quirin Schroll qs.il.paperinik at gmail.com
Tue Feb 4 15:19:38 UTC 2025


On Tuesday, 28 January 2025 at 12:38:25 UTC, Dukc wrote:
> On Thursday, 23 January 2025 at 16:20:21 UTC, Quirin Schroll 
> wrote:
>> On Monday, 13 January 2025 at 16:13:10 UTC, Dukc wrote:
>>>> D's CTFE does not allow undefined behavior.
>>>
>>> It's pretty simple in D since it has the @safe subset where 
>>> everything is defined behaviour anyway.
>>
>> That’s simply wrong. `@safe` code can call `@trusted` code and 
>> that can execute undefined behavior if it has a bug.
>
> Yes, if we're precise about it.
>
> It doesn't contradict what I meant though. Since D has `@safe`, 
> things like overflows, uninitialised variables, underflows, 
> attempting to modify a string literal etc. have to be defined 
> behaviour. The C standard mostly handles these by saying 
> "Undefined behaviour. Just don't do it." but the D spec can't, 
> otherwise `@safe` wouldn't do what it's supposed to, CTFE or no.
>
> Because of that, the D spec doesn't require a lot of paper to 
> accomodate for CTFE, but it would require a big overhaul of the 
> C spec unless it can allow compile-time undefined behaviour 
> somehow.

What is CTFE-able in D is pretty vague and includes UB. C++, from 
C++11 onward, went through all hurdles defining what `constexpr` 
included and what it doesn’t, making sure to absolutely catch any 
UB. That means two things:
- Ideally, `constexpr` is consistent over all compilers, and for 
the most part, it actually is.
- Things that obviously could be `constexpr` sometimes aren’t 
(e.g. `std::bitset` has `constexpr` support since C++23, but I 
see no reason why it can’t have it in C++11, except the 
`to_string` function).

D’s approach to CTFE is maximal pragmatism. If control-flow 
reaches a statement that cannot be executed at CTFE (for possibly 
many reasons among which are UB and a call to an `extern` 
function) it errors. There’s no attempt to specify what is and 
isn’t included. And it’s not even enforced in all cases.

The simplest form of UB is violating `const`; it’s easily 
observed when it happens. Let’s see for C++:
```cpp
constexpr int& f(const int& x)
{
     // UB if `x` is actually `const`
     // OK if `x` is actually mutable
     return const_cast<int&>(x) = 0;
}

constexpr int g(bool ub)
{
     const int x = 10;
     int y = 10;
     return ub ? f(x) : f(y);
}

static_assert(g(0) == 0); // passes
static_assert(g(1) == 0); // error, executes UB
```

Now D:
```d
ref int f(const ref int x) => cast()x = 0;

int g(bool ub)
{
     immutable int x = 10;
     int y = 10;
     return ub ? f(x) : f(y);
}

int h(bool ub)
{
     immutable int x = 10;
     int y = 10;
     if (ub)
     {
         f(x);
         return x;
     }
     else
     {
         f(y);
         return y;
     }
}

static assert(g(0) == 0); // passes, executes UB
static assert(g(1) == 0); // passes, executes UB

static assert(h(0) == 0); // passes, executes UB
static assert(h(1) == 0); // fails (h(1) == 10), executes UB
```
The issue here is, another compiler might give you `0` or `10` 
for any of these. Modifying `const` objects is not implementation 
defined, which would be the only way to justify it.

---

Ideally, CTFE-ing a function tests that the taken control-flow 
path is UB-free. In C++, one can do that. In D, it seems, one 
cannot rely on that. If we could, `@trusted` would be a lot 
better, actually, as one could compile-time test at least some 
`@trusted` functions.


More information about the Digitalmars-d mailing list