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