Search code examples
c++raii

How to ensure that a given action is reverted before leaving the function?


Here's the context. I'm working on a Qt application that draws stuff. At some point, the logic may require to perform a rotation or a translation of the scene in order to draw specific objects, given that this rotation and/or translation should necessarily be reset before drawing other objects.

Recently, I found a bug where the objects weren't drawn correctly. I ended up by finding the cause, in the piece of code which looks like this:

void example(const QString &text) {
    ...  // A few lines of code.

    this->setRotation(angle);

    ...  // A few lines of code.

    if (text.trimmed().length() == 0) {
        return;
    }

    ...  // A few more lines of code.

    this->resetRotation();
}

The error was that I forgot to call resetRotation before the return in the if block.

What would be an idiomatic way, in modern C++, to prevent such cases from happening?

RAII-like pattern comes into mind, and I could imagine for setRotation to return an object which would automatically call resetRotation in its destructor—just like std::unique_ptr does with the pointer it holds. This, however, doesn't seem particularly clear from the perspective of the caller of setRotation. One may be forced to create a variable to store the actual result of setRotation if [[nodiscard]] attribute is used, but having a variable which has no other purpose than the cleanup (and which won't be called) would, I suppose, look weird.

Does it?

What are the alternatives?


Solution

  • Instead of changing the semantics of resetRotation which might surprise some users, you can add an RAII wrapper which makes it very clear.

    Something like:

    template <typename T>
    class AutoRotationReseter {
    public:
        AutoRotationReseter(T& obj, double angle) : m_obj(obj) {
            m_obj.setRotation(angle);
        }
    
        ~AutoRotationReseter() {
            m_obj.resetRotation();
        }
    
        // Disable copy/move/assignment:
        AutoRotationReseter(AutoRotationReseter const &) = delete;
        AutoRotationReseter(AutoRotationReseter &&) = delete;
        AutoRotationReseter& operator=(AutoRotationReseter const &) = delete;
        AutoRotationReseter& operator=(AutoRotationReseter &&) = delete;
    
    private:
        T & m_obj;
    };
    

    And then use it like e.g. this (following the code you posted):

    {
        AutoRotationReseter reseter(*this, angle);
        // ...
    } // scope ends => AutoRotationReseter will reset rotation
    

    Live demo

    Notes:

    1. In order to respect the rule of 0/3/5 the copy/move/assignment operators are deleted (using them does not fit such a RAII warpper anyway).

    2. I wrote it as a template to be more generic and also because I did not know your actual class. If you need it for a specific class only, you can drop the template and simply replace T with your actual class.