Stefano Falasca
Stefano Falasca

Reputation: 9097

Providing strong guarantees in methods/classes that were not designed with exception-safety in mind

I have a design problem. Let's start by saying that this code

struct Trial{
    const double a;
    Trial(double a_) : a(a_){}
};

int main(){
    Trial t1(1.);
    Trial t2(2.);
    t1 = t2;
}

doesn't compile, because Trial::operator= isn't built by default by the compiler, since Trial::a is const. And that's pretty much obvious.

Now the point is this, code first

struct Trial{
    const double a;
    Trial(double a_) : a(a_){}
};

struct MyClass : private Trial{
    MyClass(double a_) : Trial(a_), i(0) {};
    void wannaBeStrongGuaranteeMemberFunction(){
        MyClass old(i);
        bool failed = true;
        //... try to perform operations here
        if(failed)
            *this = old;
    }
    unsigned int i;
};

int main(){
    MyClass m1(1.);
    m1.wannaBeStrongGuaranteeMemberFunction();
}

I need to provide strong exception safety for some of the methods of a class, which is derived from Trial. Such methods perform an endless series of operations on an endless series of members (i in the example) and this makes it unpractical to "manually" revert the operations. Therefore I decided that it's better to perform a copy of the whole class and copy it back if anything fails.

Small parenthesis, the code is just an example. Everything is much more complex in the legacy real-world code. In this example, copying just i would be fine, but this is not the case in the real code. Moreover, the operations have multiple (and complex) paths of executions, so that it would be a pain to "read" them as "transactions". Moreover, I'm implementing this using scope guards, so exceptions are being managed correctly in the real code.

Of course the whole thing doesn't compile, because of the line *this = old.

How would you solve the problem/address the issue?

Upvotes: 1

Views: 78

Answers (1)

James Kanze
James Kanze

Reputation: 153919

The obvious answer is to modify Trial so that it supports assignment as well. Barring that, and if the only reason you want to support assignment is to provide the strong guarantee, you could implement your own assignment operator, preferrably private, which ignored the base class; since you know that the two base classes will be identical, there's no need to assign between them.

Note that the strong guarantee more often involves swapping than assignment. Which doesn't change the problem: you can't swap two versions of Trial, either. You'ld most likely have something like:

class MyClass : private Trial
{
    class Protected
    {
        bool myCommitted;
        MyClass* myOwner;
        MyClass myInstance;
    public:
        MyClass( MyClass& owner )
            : myCommitted( false )
            , myOwner( &owner )
            , myInstance( owner )
        {
        }
        ~MyClass()
        {
            if ( myCommitted ) {
                myOwner->swap( myInstance );
            }
        }
        MyClass& instance() { return myInstance; }
        void commit() { myCommitted = true; }
    };

public:

    void protectedFunc()
    {
        Protected p( *this );
        p.instance().unprotecedVersionOfFunc();
        //  ...
        p.commit();
    }

Any exception will leave the object unchanged. (You can, of course, reverse the logic, making the modifications on this, and swapping if uncommitted.) The advantage of doing things this way is that you will also "undo" any changes in memory allocation, etc.

Finally: do you really want to use a const member in Trial. The usual way of implementing this would be to make a non-const but private, and only provide a getter. This means that Trial::a is effectively const, except for complete assignment.

Upvotes: 2

Related Questions