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++:
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:
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.