August 15, 2021

On Sunday, 15 August 2021 at 03:45:07 UTC, Mathias LANG wrote:

>

On Sunday, 15 August 2021 at 02:09:08 UTC, Tejas wrote:

>

[...]

You can't really have @nogc allocated Exception without circumventing the type system. Personally I gave up on @nogc just because of how inconvenient it is.

[...]

You think I should bother with gc_stats or stick to DRT-gcopt=verbose:2?

I'm trying to transpile the C++98 standard library so that we finally have a true @nogc standard alternative. C++98 and not C++11/14/17 because DIP 1040 is stuck in limbo.

That is why I'm insisting on pure @nogc stuff

August 15, 2021
On Sunday, 15 August 2021 at 00:15:32 UTC, Ali Çehreli wrote:

> Even though this feature is probably never used, in D, multiple exception objects are chained. For example, you can throw e.g. in a destructor when there is an active exception in flight and that second object gets attached to the first one in linked list fashion.
>
> This may be useful in some cases but in general, these colatteral exceptions don't carry much information and I don't think anybody looks at them. Usually, the first one is the one that explains the error case.
That is just an assumption. There could be designs where original exception gets wrapped in another one to comply with some interface, and in such cases, having entire chain visible, is useful. Also exceptions carry the stack trace which is useful, in debugging, allowing you to know possible location of the bug.

Regarding exception chaining, do you mean that it will automatically get chained, even without explicitly passing it as constructor of wrapping exception?

If so, it indeed might be best to remove such functionality, and just force user to do this by himself. He will then be able to decide whether chained exception does or does not carry any useful meaning.

Regards,
Alexandru


August 15, 2021
On 8/15/21 2:10 AM, Alexandru Ermicioi wrote:

>> This may be useful in some cases but in general, these colatteral
>> exceptions don't carry much information and I don't think anybody
>> looks at them. Usually, the first one is the one that explains the
>> error case.

> That is just an assumption.

Agreed but it's based on hands-on experience as well as exposure to these forums. :)

> There could be designs where original
> exception gets wrapped in another one

Wrapping is different and yes, it is useful. There have been cases where I hit a ConvException which only tells me a conversion failed. I do catch and augment it in outer contexts to saying something similar ot "That happened while doing this".

> Regarding exception chaining, do you mean that it will automatically get
> chained, even without explicitly passing it as constructor of wrapping
> exception?

Yes. That's the functionality which isn't very useful because the collateral exceptions are usually because of the main one: Imagine the file system is full and a destructor cannot flush a file. The destructor's error is not interesting in this case.

class Main : Exception {
  this() {
    super("Main failed");
  }
}

class Dtor : Exception {
  this() {
    super("The destructor failed");
  }
}

struct S {
  ~this() {
    throw new Dtor();
  }
}

import std.stdio;

void main() {
  try {
    auto s = S();
    throw new Main();

  } catch (Exception exc) {
    stderr.writeln("Failed: ", exc.msg);
    stderr.writeln("This failed too: ", exc.next.msg);
    // (Of course, real code should stop when 'next' is null.)
  }
}

That output contains two automatically chained exceptions:

Failed: Main failed
This failed too: The destructor failed

Ali

August 15, 2021
On Sunday, 15 August 2021 at 16:23:25 UTC, Ali Çehreli wrote:
> That output contains two automatically chained exceptions:
>
> Failed: Main failed
> This failed too: The destructor failed
>
> Ali

Hmm, wasn't aware of such use case (results of too much java :)). Considering this case I'd say it is better to keep it, because having more info than less is better for debugging. Even in your example, you already catch an use case that wasn't accounted for, that may or may not require fixing, i.e. it is better to know it then be in blissfull unawareness.

Though it is annoying to view those chained stacks, since they have repetitions. It would be nice if stack traces of nested exceptions would just show lines up to next exception thrown similar to how java does.

Regards,
Alexandru
August 15, 2021

On Sunday, 15 August 2021 at 16:23:25 UTC, Ali Çehreli wrote:

>

On 8/15/21 2:10 AM, Alexandru Ermicioi wrote:

> >

This may be useful in some cases but in general, these
colatteral
exceptions don't carry much information and I don't think
anybody
looks at them. Usually, the first one is the one that
explains the
error case.

>

That is just an assumption.

Agreed but it's based on hands-on experience as well as exposure to these forums. :)

>

There could be designs where original
exception gets wrapped in another one

Wrapping is different and yes, it is useful. There have been cases where I hit a ConvException which only tells me a conversion failed. I do catch and augment it in outer contexts to saying something similar ot "That happened while doing this".

>

Regarding exception chaining, do you mean that it will
automatically get
chained, even without explicitly passing it as constructor of
wrapping
exception?

Yes. That's the functionality which isn't very useful because the collateral exceptions are usually because of the main one: Imagine the file system is full and a destructor cannot flush a file. The destructor's error is not interesting in this case.

class Main : Exception {
this() {
super("Main failed");
}
}

class Dtor : Exception {
this() {
super("The destructor failed");
}
}

struct S {
~this() {
throw new Dtor();
}
}

import std.stdio;

void main() {
try {
auto s = S();
throw new Main();

} catch (Exception exc) {
stderr.writeln("Failed: ", exc.msg);
stderr.writeln("This failed too: ", exc.next.msg);
// (Of course, real code should stop when 'next' is null.)
}
}

That output contains two automatically chained exceptions:

Failed: Main failed
This failed too: The destructor failed

Ali

Do you see anything wrong with the following emplace-allocated, RAII following exceptions:

import std;
import core.stdc.stdlib;

class Main : Exception {
  this() @nogc{
    super("Main Failed");
  }
}

class Dtor : Exception {
  this() @nogc{
    super("The destructor failed");
  }
}

T heapAllocate(T, Args...)(Args args)@nogc{
    auto size = __traits(classInstanceSize, T);
    auto memory = malloc(size)[0 .. size];
    auto instance = emplace!(T,Args)(memory, args);
    return instance;
}

struct S {
  ~this()@nogc {
    scope a = heapAllocate!Dtor();
    throw a;
  }
}

void main() @nogc{
  try {
    auto s = S();
    scope a = heapAllocate!Main();
    throw a;

  } catch (Exception exc) {
    printf("Failed: %s\n", cast(char*)exc.msg);
    printf("This failed too: %s\n", cast(char*)exc.next.msg);
    // (Of course, real code should stop when 'next' is null.)
  }
}

Is this good enough for general use now? Any other drawbacks?

August 15, 2021

On Sunday, 15 August 2021 at 18:47:27 UTC, Tejas wrote:

>

Do you see anything wrong with the following emplace-allocated, RAII following exceptions:

[...]

Is this good enough for general use now? Any other drawbacks?

It only works if you're throwing and catching in the same function. Otherwise you are essentially returning a pointer to an expired stack frame, which is UB.

August 16, 2021

On Sunday, 15 August 2021 at 20:23:03 UTC, Paul Backus wrote:

>

On Sunday, 15 August 2021 at 18:47:27 UTC, Tejas wrote:

>

Do you see anything wrong with the following emplace-allocated, RAII following exceptions:

[...]

Is this good enough for general use now? Any other drawbacks?

It only works if you're throwing and catching in the same function. Otherwise you are essentially returning a pointer to an expired stack frame, which is UB.

Agh >_<

if I remove the scopeand replace it with auto?
No longer having anything to do with the stack or RAII, just using malloc + emplace instead of GC?

Yeah it might leak memory unless the catch block explicitly frees the exception object, but other than that?

August 16, 2021

On Monday, 16 August 2021 at 02:26:04 UTC, Tejas wrote:

>

Agh >_<

if I remove the scopeand replace it with auto?
No longer having anything to do with the stack or RAII, just using malloc + emplace instead of GC?

Yeah it might leak memory unless the catch block explicitly frees the exception object, but other than that?

Yeah, other than needing to manually free the exception it should be fine.

Though at that point, you might just be better off using statically-allocated exceptions.

August 16, 2021

On Sunday, 15 August 2021 at 20:23:03 UTC, Paul Backus wrote:

>

On Sunday, 15 August 2021 at 18:47:27 UTC, Tejas wrote:

>

Do you see anything wrong with the following emplace-allocated, RAII following exceptions:

[...]

Is this good enough for general use now? Any other drawbacks?

It only works if you're throwing and catching in the same function. Otherwise you are essentially returning a pointer to an expired stack frame, which is UB.

Guess I should've verified before accepting this as true.


import std;
import core.lifetime:emplace;
import core.stdc.stdlib:malloc;

T heapAllocate(T, Args...)(Args args)@nogc{
    auto size = __traits(classInstanceSize, T);
    auto memory = malloc(size)[0 .. size];
    auto instance = emplace!(T,Args)(memory, args);
    return instance;
}

void throws()@nogc{
    scope a = heapAllocate!(Exception)("works fine with scope, apparently");
    throw a;
}


void main()@nogc
{
    try{
        throws();
    } catch(Exception exp){
        printf("%s", cast(char*)exp.msg);
    }
}

That is why I was using heapAllocate, because using new on scope allocates exception on stack, and if you exit, then the exception is basically freed before it even gets caught. It fails even when you catch within the same function.

But allocate on heap and giving ownership to a scope qualified variable is no problem at all. No signal 11 killing my process ^_^

August 16, 2021

On Monday, 16 August 2021 at 05:36:07 UTC, Tejas wrote:

>

That is why I was using heapAllocate, because using new on scope allocates exception on stack, and if you exit, then the exception is basically freed before it even gets caught. It fails even when you catch within the same function.

But allocate on heap and giving ownership to a scope qualified variable is no problem at all. No signal 11 killing my process ^_^

You are relying on an accept-invalid though. The compiler should not accept that code, but currently erroneously does so.