Truly @nogc Exceptions?
Steven Schveighoffer
schveiguy at gmail.com
Wed Sep 19 21:16:00 UTC 2018
Given dip1008, we now can throw exceptions inside @nogc code! This is
really cool, and helps make code that uses exceptions or errors @nogc.
Except...
The mechanism to report what actually went wrong for an exception is a
string passed to the exception during *construction*. Given that you
likely want to make such an exception inside a @nogc function, you are
limited to passing a compile-time-generated string (either a literal or
one generated via CTFE).
To demonstrate what I mean, let me give you an example member function
inside a type containing 2 fields, x and y:
void foo(int[] arr)
{
auto x = arr[x .. y];
}
There are 2 ways this can throw a range error:
a) x > y
b) y > arr.length
But which is it? And what are x and y, or even the array length?
The error message we get is basic (module name and line number aren't
important here):
core.exception.RangeError at testerror.d(6): Range violation
Not good enough -- we have all the information present to give a more
detailed message. Why not:
Attempted slice with wrong ordered parameters, 5 .. 4
or
Slice parameter 6 is greater than length 5
All that information is available, yet we don't see anything like that.
Let's look at the base of all exception and error types to see why we
don't have such a thing. The part which prints this message is the
member function toString inside Throwable, repeated here for your
reading pleasure [1]:
void toString(scope void delegate(in char[]) sink) const
{
import core.internal.string : unsignedToTempString;
char[20] tmpBuff = void;
sink(typeid(this).name);
sink("@"); sink(file);
sink("("); sink(unsignedToTempString(line, tmpBuff, 10));
sink(")");
if (msg.length)
{
sink(": "); sink(msg);
}
if (info)
{
try
{
sink("\n----------------");
foreach (t; info)
{
sink("\n"); sink(t);
}
}
catch (Throwable)
{
// ignore more errors
}
}
}
(Side Note: there is an overload for toString which takes no delegate
and returns a string. But since this overload is present, doing e.g.
writeln(myEx) will use it)
Note how this *doesn't* allocate anything.
But hang on, what about the part that actually prints the message:
sink(typeid(this).name);
sink("@"); sink(file);
sink("("); sink(unsignedToTempString(line, tmpBuff, 10));
sink(")");
if (msg.length)
{
sink(": "); sink(msg);
}
Hm... Note how the file name, and the line number are all *members* of
the exception, and there was no need to allocate a special string to
contain the message we saw. So it *is* possible to have a custom message
without allocation. It's just that the only interface for details is via
the `msg` string member field -- which is only set on construction.
We can do better.
I noticed that there is a @__future member function inside Throwable
called message. This function returns the message that the Throwable is
supposed to display (defaulting to return msg). I believe this was
inserted at Sociomantic's request, because they need to be able to have
a custom message rendered at *print* time, not *construction* time [2].
This makes sense -- why do we need to allocate some string that will
never be printed (in the case where an exception is caught and handled)?
This helps alleviate the problem a bit, as we could construct our
message at print-time when the @nogc requirement is no longer present.
But we can do even better.
What if we added ALSO a function:
void message(scope void delegate(in char[]) sink)
In essence, this does *exactly* what the const(char)[] returning form of
message does, but it doesn't require any allocation, nor storage of the
data to print inside the exception. We can print numbers (and other
things) and combine them together with strings just like the toString
function does.
We can then replace the code for printing the message inside toString
with this:
bool printedColon = false;
void subSink(in char[] data)
{
if(!printedColon && data.length > 0)
{
sink(": ");
printedColon = true;
}
sink(data);
}
message(&subSink);
In this case, we then have a MUCH better mechanism to implement our
desired output from the slice error:
class RangeSliceError : Throwable
{
size_t lower;
size_t upper;
size_t len;
...
override void message(scope void delegate(in char[]) sink)
{
import core.internal.string : unsignedToTempString;
char[20] tmpBuff = void;
if (lower > upper)
{
sink("Attempted slice with wrong ordered parameters ");
sink(unsignedToTempString(lower, tmpBuff, 10));
sink(" .. ");
sink(unsignedToTempString(upper, tmpBuff, 10));
}
else if (upper > len)
{
sink("Slice parameter ");
sink(unsignedToTempString(upper, tmpBuff, 10));
sink(" is greater than length ");
sink(unsignedToTempString(len, tmpBuff, 10));
}
else // invalid parameters to this class
sink("Slicing Error, but unsure why");
}
}
And look Ma, no allocations!
So we can truly have @nogc exceptions, without having requirements to
use the GC on construction. I think we should add this.
One further thing: I didn't make the sink version of message @nogc, but
in actuality, it could be. Notice how it allocates using the stack. Even
if we needed some indeterminate amount of memory, it would be simple to
use C malloc/free, or alloca. But traditionally, we don't put any
attributes on these base functions. Would it make sense in this case?
As Andrei says -- Destroy!
-Steve
[1] -
https://github.com/dlang/druntime/blob/542b680f2c2e09e7f4b494898437c61216583fa5/src/object.d#L2642
[2] - https://github.com/dlang/druntime/pull/1895
More information about the Digitalmars-d
mailing list