C++ exception safety paranoia: how much is too much?
Asked Answered
A

4

2

The strong exception safety guarantee says that an operation won't change any program state if an exception occurs. An elegant way of implementing exception-safe copy-assignment is the copy-and-swap idiom.

My questions are:

  1. Would it be overkill to use copy-and-swap for every mutating operation of a class that mutates non-primitive types?

  2. Is performance really a fair trade for strong exception-safety?

For example:

class A
{   
    public:
        void increment()
        {   
            // Copy
            A tmp(*this);

            // Perform throwing operations on the copy
            ++(tmp.x);
            tmp.x.crazyStuff();

            // Now that the operation is done sans exceptions, 
            // change program state
            swap(tmp);
        }   

        int setSomeProperty(int q)
        {   
            A tmp(*this);
            tmp.y.setProperty("q", q); 
            int rc = tmp.x.otherCrazyStuff();
            swap(tmp);
            return rc;
        }   

        //
        // And many others similarly
        //

        void swap(const A &a) 
        {   
            // Non-throwing swap
        }   

    private:
        SomeClass x;
        OtherClass y;
};
Angloamerican answered 28/3, 2012 at 14:30 Comment(4)
The answer depends greatly on your domain, and cannot be answered outside of it. If a life depends on it, the answer is obvious: it is worth the performance penalty. If the exception handling code will destroy your object in the event of an exception, it will not matter. Now you have to find where in the middle your code lays.Creech
If you factor your code properly into single-responsibility components, you often get a lot of exception safety "for free". In your example, why does an "increment" function do "crazy stuff"? Either the incrementing algorithm is complex and knows how to do the computation transactionally, or you need to separate incrementing and crazystuffing.Calisaya
Well you could use the same idea and just apply it to the members. Doesn't need to be the whole thing. What if say your class contains a vector of strings, and you only need to change one? Don't copy/swap the whole class, vector and all. Just do it on the one string.Romp
Side note: Make sure you really want exceptions before starting using them everywhere. See this questionAsparagus
D
2

As all matters of engineering, it is about balance.

Certainly, const-ness/immutability and strong guarantees increase confidence in one's code (especially accompanied with tests). They also help trim down the space of possible explanations for a bug.

However, they might have an impact on performance.

Like all performance issues, I would say: profile and get rid of the hot spots. Copy And Swap is certainly not the only way to achieve transactional semantics (it is just the easiest), so profiling will tell you where you should absolutely not use it, and you will have to find alternatives.

Dykes answered 28/3, 2012 at 14:36 Comment(0)
B
3

You should always aim for the basic exception guarantee: make sure that in the event of an exception, all resources are released correctly and the object is in a valid state (which can be undefined, but valid).

The strong exception guarantee (ie. "transactions") is something you should implement when you think it makes sense: you don't always need transactional behavior.

If it is easy to achieve transactional operations (eg. via copy&swap), then do it. But sometimes it is not, or it incurs a big perfomance impact, even for fundamental things like assignment operators. I remember implementing something like boost::variant where I could not always provide the strong guarantee in the copy assignment.

One tremendous difficulty you'll encounter is with move semantics. You do want transactions when moving, because otherwise you lose the moved object. However, you cannot always provide the strong guarantee: think about std::pair<movable_nothrow, copyable> (and see the comments). This is where you have to become a noexcept virtuoso, and use an uncomfortable amount of metaprogramming. C++ is difficult to master precisely because of exception safety.

Batty answered 28/3, 2012 at 14:35 Comment(2)
What's the problem with std::pair<copyable, movable_nothrow>? As you say, it's difficult to write pair such that all nothrow operations are either done last or (worse) reversed when the non-nothrow operation throws. But in the case of one nothrow op and one that itself offers the strong exception guarantee, there does exist a correct sequence of operations.Intensity
@SteveJessop: Yes, good point. However, the move constructor of std::pair will not be noexcept. Also, this kind of makes my point: exception safety is difficult, and move semantics require a great deal of template metaprogramming Fu. How would you write the move constructor of a std::pair<movable_nothrow, copyable> (note the reversing of the arguments) ? You want to construct second before first but this requires that they are declared in this order.Batty
D
2

As all matters of engineering, it is about balance.

Certainly, const-ness/immutability and strong guarantees increase confidence in one's code (especially accompanied with tests). They also help trim down the space of possible explanations for a bug.

However, they might have an impact on performance.

Like all performance issues, I would say: profile and get rid of the hot spots. Copy And Swap is certainly not the only way to achieve transactional semantics (it is just the easiest), so profiling will tell you where you should absolutely not use it, and you will have to find alternatives.

Dykes answered 28/3, 2012 at 14:36 Comment(0)
B
1

It depends on what environment your application is going to run in. If you just run it on your own machine (one end of the spectrum), it might not be worth to be too strict on exception safety. If you are writing a program e.g. for medical devices (the other end), you do not want unintentional side-effects left behind when an exception occurs. Anything in-between is dependent on the level of tolerance for errors and available resources for development (time, money, etc.)

Breskin answered 28/3, 2012 at 14:34 Comment(0)
M
0

Yes, the problem you are facing is that this idiom is very hard to scale. None of the other answers mentioned it, but another very interesting idiom invented by Alexandrescu called scopeGuards. It helps to enhance the economy of the code and makes huge improvements in readability of functions that need to conform to strong exception safety guarantees.

The idea of a scope guard is a stack instance that lets just attach rollback function objects to each resource adquisition. when the scope guard is destructed (by an exception) the rollback is invoked. You need to explicitly call commit() in normal flow to avoid the rollback invocation at scope exit.

Check this recent question from me that is related to designed a safe scopeguard using c++11 features.

Mulford answered 23/4, 2012 at 17:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.