How does this implementation of chaining exceptions work?
Asked Answered
R

1

3

I previously asked a question about how to chaining exceptions in C++, and one of the answers provided a nifty solution to how it can be done. The problem is that I don't understand the code, and trying to have this kind of discussion in the comments is just too much of a bother. So I figured it's better to start a new question entirely.

The code is included below and I've clearly marked each section which I don't get. A description of what I don't understand is included below the code. The code was written by Potatoswatter.


Code


struct exception_data { // abstract base class; may contain anything
    virtual ~exception_data() {}
};

struct chained_exception : std::exception {
    chained_exception( std::string const &s, exception_data *d = NULL )
        : data(d), descr(s) {
        try {
            link = new chained_exception;

            // ----------------------------------------------------------------
            //   How does this work (section 1)?
            throw;
            // ----------------------------------------------------------------

        } catch ( chained_exception &prev ) {
            // ----------------------------------------------------------------
            //   How does this work (section 2)?
            swap( *link, prev );
            // ----------------------------------------------------------------
        } // catch std::bad_alloc somehow...
    }

    friend void swap( chained_exception &lhs, chained_exception &rhs ) {
        std::swap( lhs.link, rhs.link );
        std::swap( lhs.data, rhs.data );
        swap( lhs.descr, rhs.descr );
    }

    virtual char const *what() const throw() { return descr.c_str(); }

    virtual ~chained_exception() throw() {
        // --------------------------------------------------------------------
        //   How does this work (section 3)?
        if ( link && link->link ) delete link; // do not delete terminator
        // --------------------------------------------------------------------
        delete data;
    }

    chained_exception *link; // always on heap
    exception_data *data; // always on heap
    std::string descr; // keeps data on heap

private:
    chained_exception() : link(), data() {}
    friend int main();
};

void f() {
    try {
        throw chained_exception( "humbug!" );
    } catch ( std::exception & ) {
        try {
            throw chained_exception( "bah" );
        } catch ( chained_exception &e ) {
            chained_exception *ep = &e;
            for ( chained_exception *ep = &e; ep->link; ep = ep->link ) {
                // Print ep->what() to std::cerr
            }
        }
    }

    try {
        throw chained_exception( "meh!" );
    } catch ( chained_exception &e ) {
        for ( chained_exception *ep = &e; ep->link; ep = ep->link ) {
            // Print ep->what() to std::cerr
        }
    }
}

int main() try {
    // ------------------------------------------------------------------------
    //   How does this work (section 4)?
    throw chained_exception(); // create dummy end-of-chain
    // ------------------------------------------------------------------------
} catch( chained_exception & ) {
    // body of main goes here
    f();
}

Running the code gives the following output:

bah
humbug!
meh!

What I don't understand

  1. throw; inside try-block: I've never seen this before. The only place I've thought throw; to be valid was inside a catch-block to rethrow what was caught. So what does this do? Some debugging apparently shows that the thrown exception is what was thrown previously, but that was inside a completely different try-block. In fact, it was even outside the struct declaration!

  2. Swap fields: Why do we need to swap the exception fields? Wouldn't just copying of the pointers be enough? Is this to prevent the structures to which the fields point at from being deleted from the heap prematurely?

  3. Check link and link's link: I can understand checking that link is not NULL (even though deleting a NULL pointer has no effect), but why the need to check the link's link?

  4. Throw dummy exception: Why is this dummy needed? It's thrown but then dropped. Why do we need this as an end to the chain?

Rust answered 25/8, 2010 at 14:18 Comment(2)
Note, this doesn't satisfy one requirement you wanted, that different types of exceptions may be thrown. That would require that every usable exception class implement a virtual method to clone itself onto the heap, and using that method in the public constructor.Hyacinthhyacintha
Ah, indeed it does. Thanks for pointing that out.Rust
C
4

Clever code - kudos to potatoswatter on this one. I think that I would have to find some way around the last item though.

  1. throw; rethrows the active exception. It is only valid if a catch block is on the stack. I can't recall where I came across that tidbit at but it was probably on SO in the context of some other question. The bare throw gives us access to the current exception by catching it in the chained_exception constructor. In other words, prev in the constructor is a reference to the exception that we are currently processing.

  2. You are correct here. This prevents double deletion.

  3. The sentinel exception, the one thrown in main, should never be deleted. The one identifying attribute of this exception is that it's link member is NULL.

  4. This is the part that I don't like but cannot think of an easy way around. The only visible chained_exception constructor can only be called when a catch block is active. IIRC, a bare throw without an active catch block is a no-no. So, the workaround is to throw in main and put all of your code in the catch block.

Now, if you try this method in multi-threaded code, make sure that you understand (4) very well. You will have to replicate this in your thread entry point.

Commonplace answered 25/8, 2010 at 14:44 Comment(6)
+1, yep, thank you. One other place you might have seen this trick is #2488009, which specifically deals with multithreading as you describe.Hyacinthhyacintha
Thank you very much for your answer, D.Shawley. Section 1 was very sneaky way to do the chaining; I didn't know you could use throw; outside a catch block as long as you at least have a catch block on the stack. Is this what causes the need for a dummy exception? What would happen if you invoked throw; outside a catch block? Section 3 makes sense now; of course you would not want to try deleting the active exception. That would wreck havoc indeed.Rust
@gablin: yep, throw; with no catch "on the stack" results in immediate termination of the program.Hyacinthhyacintha
@Potatoswatter: I see. Couldn't we solve this by also providing a pointer to the child exception in the constructor? What I mean is, instead of doing a throw-and-catch inside constructor chained_exception(std::string const &s, exception_data *d = NULL), we augment the constructor with chained_exception(std::string const s&, exception_data *d = NULL, exception* prev = NULL), thus avoiding always having a "live" exception?Rust
@gablin: the fundamental problem, and the motivation for doing it this way, is that every operation that may throw won't always have a pointer to the current exception. Ensuring that they do will either take a lot of cumbersome argument passing (the antithesis to native exceptions), or a global variable that entirely precludes multithreading.Hyacinthhyacintha
@Potatoswatter: Ah. But is that common? I mean, is such a feature really that necessary that we must put up with having the "null" exception?Rust

© 2022 - 2024 — McMap. All rights reserved.