Should exceptions be chained in C++? [duplicate]
Asked Answered
E

4

13

I just finished work on a C++-program where I've implemented my own exceptions (although derived from std::exception). The practice I've applied when one exception causes a chain reaction, propagating the error upwards and giving rise to other exceptions, is to concatenate the error message at each appropriate step across the modules (read classes). I.e. the old exception itself is dropped and a new exception is created, but with a longer error message.

This may have worked for my small program, but I wasn't very satisfied with my approach in the end. For one, line numbers (although not applied at the moment) and file names are not retained except for the last exception; and really that information is of most interest in the first exception.

I figure this could have been handled better by chaining exceptions together; i.e. the old exception is provided in the constructor of the new exception. But how would that be implemented? Does not exceptions die when they go out of scope from the method, thereby preventing one to use exception pointers? And how to copy and store the exception if the exception can be of any derived class?

This ultimately lead me to consider whether chaining exceptions in C++ is such a good idea after all. Perhaps one should just create one exception and then add additional data to that (like I've been doing, but probably in a much better manner)?

What is your response to this? Should exceptions caused by another be chained together to retain a sort of "exception trace" -- and how should that be implemented? -- or should a single exception be used and additional data attached to it -- and how should that be done?

Edmondedmonda answered 22/8, 2010 at 22:31 Comment(3)
@kbrimington: Indeed, that question does touch upon the core of this question; i.e. chaining of exceptions (or "inner exceptions"); my question only expands on that and also asks whether such undertakings should be done to begin with, or stick with the thou-shall-throw-only-one approach.Edmondedmonda
Forgot to thank you for the link, but the comment could no longer be edited. ^^Edmondedmonda
As the answer to the question what is the proper way to do exception chaining in C++ is to use the C++11 std::nested_exception class, evidently the writers of the C++ standard library believe that, yes, chaining exceptions can be a good thing to do.Longe
L
2

It is necessary to copy the data out of an exception object, into a chain, if you want it to outlive the catch block that receives it, aside from rethrow by throw;. (Which includes, for example, if that catch block exits through a throw obj;.)

This can be done by putting data to be saved on the heap, and implementing swap (move in C++0x) on your private data inside the exception, for example.

Of course, you need to be careful when using the heap with exceptions… but then again, in most modern OSes, memory overcommitment completely prevents new from ever throwing, for better or for worse. A good memory margin and dropping exceptions from the chain upon complete meltdown should keep it safe.

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;
            throw;
        } catch ( chained_exception &prev ) {
            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() {
        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 ) {
                std::cerr << ep->what() << std::endl;
            }
        }
    }

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

int main() try {
    throw chained_exception(); // create dummy end-of-chain
} catch( chained_exception & ) {
    // body of main goes here
    f();
}

output (appropriately grumpy):

bah
humbug!
meh!
Legault answered 22/8, 2010 at 23:35 Comment(8)
I take it a "swap" or "move" is equivalent to a Java "clone", yes?Edmondedmonda
@gablin: I don't know Java, but I don't think so. You want the object to have a pointer to the data to be saved. swap and move assign that pointer from the caught object to the object on the heap without copying anything (which could be risky). The caught object may then be destroyed without affecting that data.Legault
@Potatoswatter: But that would only be useful if the data stored in the caught exception is stored on the heap already, correct? If not, then that data will be destroyed whence the exception is destroyed, and if you did not copy that data but only saved it by reference, then you would have a very unsafe situation with pointers pointing to invalid memory. For instance, the error message must then be stored in the exception as a string*, not a string. Or have I misunderstood the whole thing...?Edmondedmonda
@gablin: My point is that it is best to place it on the heap upon creation. However, a non-pointer std::string does satisfy this, and moreover string implements both swap and move.Legault
@Potatoswatter: I'm confused. Could you please give a short example which explains the difference?Edmondedmonda
@gablin: here you go. Actually, it's not short, it's pretty fully functional. There isn't a way to demonstrate the semantics of exception object creation and destruction without really running through it all.Legault
@Potatoswatter: Cool! It actually works! Now I just have to figure out how it works... Thanks a bunch! ^^Edmondedmonda
@Potatoswatter: I just can't figure it out. Even with a debugger. I've asked a question (#3567274) if you can take the time and effort of explaining it further. ^^Edmondedmonda
V
6

Since this question has been asked noteable changes have been made to the standard with C++11. I am continually missing this in discussions about exceptions, but the following approach, nesting exceptions, does the trick:

Use std::nested_exception and std::throw_with_nested

It is described on StackOverflow here and here, how you can get a backtrace on your exceptions inside your code without need for a debugger or cumbersome logging, by simply writing a proper exception handler which will rethrow nested exceptions.

Since you can do this with any derived exception class, you can add a lot of information to such a backtrace! You may also take a look at my MWE on GitHub, where a backtrace would look something like this:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"
Viguerie answered 26/10, 2017 at 8:32 Comment(0)
L
2

It is necessary to copy the data out of an exception object, into a chain, if you want it to outlive the catch block that receives it, aside from rethrow by throw;. (Which includes, for example, if that catch block exits through a throw obj;.)

This can be done by putting data to be saved on the heap, and implementing swap (move in C++0x) on your private data inside the exception, for example.

Of course, you need to be careful when using the heap with exceptions… but then again, in most modern OSes, memory overcommitment completely prevents new from ever throwing, for better or for worse. A good memory margin and dropping exceptions from the chain upon complete meltdown should keep it safe.

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;
            throw;
        } catch ( chained_exception &prev ) {
            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() {
        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 ) {
                std::cerr << ep->what() << std::endl;
            }
        }
    }

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

int main() try {
    throw chained_exception(); // create dummy end-of-chain
} catch( chained_exception & ) {
    // body of main goes here
    f();
}

output (appropriately grumpy):

bah
humbug!
meh!
Legault answered 22/8, 2010 at 23:35 Comment(8)
I take it a "swap" or "move" is equivalent to a Java "clone", yes?Edmondedmonda
@gablin: I don't know Java, but I don't think so. You want the object to have a pointer to the data to be saved. swap and move assign that pointer from the caught object to the object on the heap without copying anything (which could be risky). The caught object may then be destroyed without affecting that data.Legault
@Potatoswatter: But that would only be useful if the data stored in the caught exception is stored on the heap already, correct? If not, then that data will be destroyed whence the exception is destroyed, and if you did not copy that data but only saved it by reference, then you would have a very unsafe situation with pointers pointing to invalid memory. For instance, the error message must then be stored in the exception as a string*, not a string. Or have I misunderstood the whole thing...?Edmondedmonda
@gablin: My point is that it is best to place it on the heap upon creation. However, a non-pointer std::string does satisfy this, and moreover string implements both swap and move.Legault
@Potatoswatter: I'm confused. Could you please give a short example which explains the difference?Edmondedmonda
@gablin: here you go. Actually, it's not short, it's pretty fully functional. There isn't a way to demonstrate the semantics of exception object creation and destruction without really running through it all.Legault
@Potatoswatter: Cool! It actually works! Now I just have to figure out how it works... Thanks a bunch! ^^Edmondedmonda
@Potatoswatter: I just can't figure it out. Even with a debugger. I've asked a question (#3567274) if you can take the time and effort of explaining it further. ^^Edmondedmonda
S
1

You may want to look at this: http://www.boost.org/doc/libs/1_43_0/libs/exception/doc/boost-exception.html

It's somewhat different approach to what MS did in C#, but it seems to match your requirements.

Scrubber answered 23/8, 2010 at 6:5 Comment(4)
I've heard of boost, but never gotten around to use it. I will examine the link you posted and see if that holds the answer I am looking for. Thanks.Edmondedmonda
@Edmondedmonda - It's basically a well structured means of allowing data to be added to exceptions in the catch block. This means the exceptions you throw all have to be derived from ::boost::exception, but if they are, adding information to the exception and re-throwing with throw; as it propagates up the chain is relatively painless.Neper
gablin, boost is a must for any C++ programming today. It's been so successful that some of it is becoming a part of the new standard library (see TR1, etc..)Scrubber
Oh. Well, I knew boost was common, but not that common. Then again I can't say I've been working with C++ that much, either. I will make sure to investigate [code]::boost[/code]. Thanks again.Edmondedmonda
N
0

Another idea is to add the relevant data to your exception object then use a bare throw; statement to re-throw it. I think the stack information is retained in this case, and so you'll still know the original source of the exception, but testing would be a good idea.

I bet since whether or not any stack information is available at all is implementation defined, that implementations will vary even more widely in whether or not it's preserved in any way after a bare throw; statement.

Neper answered 22/8, 2010 at 22:43 Comment(3)
This is elegant if it applies, but the type of the exception can't change.Legault
As Potatoswatter already mentioned the type of the exception can't change, and I find that cumbersome since the interpretation of the error becomes more difficult the further up the stack it goes. For instance, catching an IndexOutOfBoundsException in the top method invoking a user-initiated action doesn't make much sense if it was thrown way down in a method happening to access an internal vector with wrong index. The only possible approach I see here is either to chain it or drop it entirely for a replacement exception.Edmondedmonda
@Edmondedmonda - Yes, that makes sense and I agree. Unfortunately I don't think there's any good way of keeping stack information about the original exception if you throw a new one. So you get a choice. I think ::boost::exception is a good way to record information in your exception, even if you throw a new one. It would allow you to record the original, and I think that would generally be a good idea.Neper

© 2022 - 2024 — McMap. All rights reserved.