try/catch idiom in std.datetime

Jonathan M Davis jmdavisProg at gmx.com
Sun Nov 17 23:28:58 PST 2013


On Sunday, November 17, 2013 22:32:46 Andrei Alexandrescu wrote:
> (It may seem I chose this example to discuss the "let's insert one empty
> line every other line" idiom. I didn't.)

That code's like that just because I like to put empty lines before and after 
if statements and before return statements, as I think that that improves 
legibility. Short functions like that suffer as a result, because they end up 
with a larger proportion of the lines being empty than is normal. I'm always a 
bit torn on that, because I don't like having quite that many empty lines in 
so few lines, but I also don't like not having space around if statements and 
return statements. I never feel like I can find a nice, legible balance with 
short functions like that.

> Essentially the code relies on
> calls that may generally throw, but calls them with parameters that
> should guarantee there won't be any throwing. In wanting to offer as
> many "nothrow" guarantees as possible, the code ends up inserting these
> try/catch statements - seemingly needlessly.

Yeah, I tried to use pure and nothrow fairly heavily in std.datetime and ran 
into a number of hurdles like this. Fortunately, the situation has improved 
somewhat (e.g. format can finally be pure at least some of the time), but it 
does show that it's not always easy to use pure or nothrow even when it 
arguably should be.

> What would be the best approach here?
> 
> 0. Do nothing. This is as good as it gets.

Well, it works just fine as-is, but it would be kind of nice to be able to 
solve the problem in a less verbose manner (though you're talking about saving 
only a few lines of code).

> 1. Fix scope(failure) and then use it.

Fine with me. I might still favor the try-catch in cases where you can clearly 
wrap it around one function call, because then you avoid problems where you've 
accidentally effectively marked the whole function as trusted-nothrow when you 
only want to mark one function call that way. But you could do the same thing 
with scope(failure) and a new scope. The main problem is when you can't really 
put the calls that need to be trusted-nothrow inside a new scope, in which 
case, you're forced to mark the whole function (or at least large portions of 
it) as trusted-nothrow by wrapping it all in a try-catch or scope(failure).

> 2. Relax the nothrow guarantees. After all, nothrow is opt-in.

I'm not quite sure what you're suggesting here. Make it so that nothrow does 
checking at runtime instead of compile time? That would be moving in the 
direction of C++ and throw specifiers (or more precisely, noexcept, I suppose). 
If that's what you're suggesting, I'd be very much against that. I think that 
the fact D's nothrow is statically checked is a huge advantage over C++'s 
noexcept. The fact that you have to sometimes use try-catch blocks (or 
scope(failure) if that works) to make it work is essentially the same as 
needing @trusted to make some stuff @safe. I wouldn't want to throw away 
@trusted in favor of making @safe more lax either (though that's almost all 
static checking which can't be done at runtime, unlike with noexcept).

Of course, there's no way to verify trusted-nothrow except at runtime like 
std.datetime is doing with try-catch and assertions, but most code _can_ be 
checked statically (including the code that calls the functions that use the 
try-catch-assert idiom to be able to be nothrow), and that's much more 
pleasant, particularly because it's actually checked by the compiler that way 
rather than just blowing up on you at runtime.

I suppose that if it were considered annoying enough to have to use try-catch 
blocks or scope(failure), we could add a nothrow equivalent to @trusted to 
mark functions with, though it's already been argued that @trusted should be 
on pieces of a function rather than on the whole function, and it would 
arguably be better to mark sections of a function as trusted-nothrow rather 
than the entire thing. try-catch lets us do that already, but it might be nice 
to be able to do the equivalent of

     @property FracSec fracSec() const nothrow
     {
         trusted-nothrow
         {
             auto hnsecs = removeUnitsFromHNSecs!"days"(adjTime);

             if(hnsecs < 0)
                 hnsecs += convert!("hours", "hnsecs")(24);

             hnsecs = removeUnitsFromHNSecs!"seconds"(hnsecs);

             return FracSec.from!"hnsecs"(cast(int)hnsecs);
         }
     }

and have the compiler insert the catch and assertion for you.

> 3. Look into API changes that add nothrow, narrower functions to the
> more general ones that may throw.

In some cases (e.g. format) that's probably a good idea, but I don't think 
that really scales. In some cases, it would be well worth it, whereas in 
others, it would just be better to use try-catch in the few places where you 
know the function won't throw, because it would be quite rare to be able to 
guarantee that it wouldn't throw. It's also not pleasant to have to duplicate 
functions all over the place.

> 4. ...?

We now have std.exception.assumeWontThrow, which works reasonably well when 
you need to wrap a single call as opposed to several, but it has the same 
problem as enforce in that it uses lazy, which is definite performance hit. So, 
in most cases, I'd be more inclined to just use a try-catch, and if it's more 
than one expression, you pretty much need to use try-catch or scope(failure) 
instead anyway, since you wouldn't want to wrap whole function bodies in a 
call to assumeWontThrow (assuming that you even could).

So, that's a partial solution, but not a perfomant one. However, we do really 
need to improve the performance of lazy, because enforce gets used all over 
the place, and it's definitely shown up as being costly in some of the profiling 
that's been shown in the newsgroup. And if that gets fixed, then using 
assumeWontThrow wouldn't be as bad.

All in all, I find the need to use try-catch blocks to effectively do trusted-
nothrow a bit annoying, but I haven't felt that it was a big enough deal to 
try and find another solution for it. Having to do trusted-purity is far worse, 
because that requires using a function pointer and casting it, but that would 
_should_ be hard to do because of how hard it is to actually guarantee that 
the function is acting like a pure function even though it isn't. And I'm not 
sure that I'd entirely trust myself with that, let alone the average D 
developer. trusted-nothrow on the other hand is something that the average 
programmer should be able to grasp.

- Jonathan M Davis


More information about the Digitalmars-d mailing list