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?

Community
  • 1
  • 1
gablin
  • 4,678
  • 6
  • 33
  • 47
  • 1
    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. – Potatoswatter Aug 25 '10 at 18:35
  • Ah, indeed it does. Thanks for pointing that out. – gablin Aug 25 '10 at 21:31

1 Answers1

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.

D.Shawley
  • 58,213
  • 10
  • 98
  • 113
  • +1, yep, thank you. One other place you might have seen this trick is http://stackoverflow.com/questions/2487509/are-c-exceptions-sufficient-to-implement-thread-local-storage, which specifically deals with multithreading as you describe. – Potatoswatter Aug 25 '10 at 18:14
  • 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. – gablin Aug 25 '10 at 21:42
  • @gablin: yep, `throw;` with no `catch` "on the stack" results in immediate termination of the program. – Potatoswatter Aug 25 '10 at 22:02
  • @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? – gablin Aug 25 '10 at 22:11
  • @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. – Potatoswatter Aug 26 '10 at 00:00
  • @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? – gablin Aug 26 '10 at 12:18