How do I run a cleanup code on the function exit?
Asked Answered
S

3

2

C++ classes provide RAII idiom. Therefore you don't have to care about exceptions:

void function()
{
    // The memory will be freed automatically on function exit
    std::vector<int> vector(1000);

    // Do some work        
}

But if you have (for some reasons) to use some pure C API, you have either to create C++ wrappers around it or to use try/catch blocks

void function()
{
    int *arr = (int*)malloc(1000*sizeof(int));
    if (!arr) { throw "cannot malloc"; }

    try
    {
        // Do some work
    }
    catch (...)
    {
        free(arr); // Free memory in case of exception
        throw;     // Rethrow the exception
    }

    // Free memory in case of success
    free(arr);
}

Even if you use C++ classes with RAII idiom, sometimes you have to write a code with strong exception-safety guaranty:

void function(std::vector<const char*> &vector)
{
    vector.push_back("hello");
    try
    {
        // Do some work

        vector.push_back("world");
        try
        {
            // Do other work
        }
        catch (...)
        {
            vector.pop_back(); // Undo vector.push_back("world")
            throw;             // Rethrow the exception
        }
    }
    catch (...)
    {
        vector.pop_back(); // Undo vector.push_back("hello");
        throw;             // Rethrow the exception
    }
}

But these constructions are quite bulky.

Is there any way to force to run some cleanup code at function exit? Something similar to atexit, but in a function scope...

Is there any way to run some rollback code in case of exception without using nested try/catch blocks?

I would like to have some operators or functions that would work like this:

void function(std::vector<const char*> &vector)
{
    int *arr = malloc(1000*sizeof(int));
    onexit { free(arr); }

    vector.push_back("hello");
    onexception { vector.pop_back(); }

    // Do some work

    vector.push_back("world");
    onexception { vector.pop_back(); }

    // Do other work
}

If it is possible to create such functions, are there any reasons to avoid using them? Are there such constructs in other programming languages?

Simsar answered 17/2, 2018 at 15:17 Comment(11)
Sounds like a good fit for scope guards.Iasis
Sorry, can you confirm why you need malloc?Plater
@LightnessRacesinOrbit, malloc is just a simple example, it could be any function from pure C library that doesn't have C++ alternative.Simsar
Ok, that makes sense then.Plater
I don't like the rollback technique itself here; wouldn't it be better to perform the push_backs on a copy of the original vector and just swap the two when the whole transaction has been completed?Emlyn
@ChristianHackl, In some cases making a copy of std::vector may be unacceptable due to performance limitations. The code in my question is just a simple example. Sometimes it is impossible to use copy-and-swap idiom at all.Simsar
Please don't call RAII an idiom. RAII has more than enough I's in it already.Sikorsky
@JiveDadson - Resource Acquisition Is Initialization Idiom - RAIII.Simsar
RAII is a bad enough name without sticking "idiom" onto it. Stop the madness! The way programmers use "idiom" is all wrong anyway. An idiom is a figure of speech, the meaning of which cannot be determined by using the ordinary rules of syntax and semantics alone. Consider this: "I brought my umbrella to work today. It was raining." What was raining, the umbrella? There is no antecedent for "it". "It was raining" is an idiom.Sikorsky
@JiveDadson: Software engineering borrows words from linguistics, where's the problem? Idioms in programming languages have roughly the same definition. Coincidentally, I believe your example is wrong. "It was raining" is not an idiom; that "it" is merely an impersonal pronoun. Examples for English idioms would be "to face the music" or "to let the cat out of the bag". In the same vein (another idiom), the meaning, or the intention of RAII transcends the sum of its parts.Emlyn
@anton_rh: Note that what I intended here has nothing to do with the copy-and-swap idiom, which concerns only copy assignment operators.Emlyn
S
2

I have created macros that implement this functionality. They generate a local variable that runs a cleanup code in the destructor using C++11 lambda functions. The std::uncaught_exception function is used to check if there is any exception currently thrown. Creating the variable itself shouldn't throw any exceptions because a lambda with all variables captured by reference is used to create the variable (such lambdas do not throw exceptions in copy/move constructors).

#include <exception>

// An object of the class below will run an arbitrary code in its destructor
template <bool always, typename TCallable>
class OnBlockExit
{
public:
    TCallable m_on_exit_handler;

    ~OnBlockExit()
    {
        if (always || std::uncaught_exception())
            { m_on_exit_handler(); }
    }
};

// It is not possible to instantiate an object of the 'OnBlockExit' class
// without using the function below: https://mcmap.net/q/24817/-how-can-i-store-a-lambda-expression-as-a-field-of-a-class-in-c-11.
// Creating of an object of the 'OnBlockExit' class shouldn't throw any exception,
// if lambda with all variables captured by reference is used as the parameter.
template <bool always, typename TCallable>
OnBlockExit<always, TCallable> MakeOnBlockExit(TCallable &&on_exit_handler)
{
    return { std::forward<TCallable>(on_exit_handler) };
}

// COMBINE is needed for generating an unique variable
// (the name of the variable contains the line number:
// https://mcmap.net/q/24818/-creating-macro-using-__line__-for-different-variable-names-duplicate)
#define COMBINE1(X,Y) X##Y
#define COMBINE(X,Y) COMBINE1(X,Y)

// ON_BLOCK_EXIT generates a variable with the name
// in the format on_block_exit##__LINE__
#define ON_BLOCK_EXIT(always, code) \
    auto COMBINE(on_block_exit,__LINE__) = MakeOnBlockExit<always>([&]()code)

// Below are target macros that execute the 'code' on the function exit.
// ON_FINALLY will allways execute the code on the function exit,
// ON_EXCEPTION will execute it only in the case of exception.
#define ON_EXCEPTION(code) ON_BLOCK_EXIT(false, code)
#define ON_FINALLY(code)   ON_BLOCK_EXIT(true , code)

Here is an example how to use these macros:

void function(std::vector<const char*> &vector)
{
    int *arr1 = (int*)malloc(800*sizeof(int));
    if (!arr1) { throw "cannot malloc arr1"; }
    ON_FINALLY({ free(arr1); });

    int *arr2 = (int*)malloc(900*sizeof(int));
    if (!arr2) { throw "cannot malloc arr2"; }
    ON_FINALLY({ free(arr2); });

    vector.push_back("good");
    ON_EXCEPTION({ vector.pop_back(); });

    auto file = fopen("file.txt", "rb");
    if (!file) { throw "cannot open file.txt"; }
    ON_FINALLY({ fclose(file); });

    vector.push_back("bye");
    ON_EXCEPTION({ vector.pop_back(); });

    int *arr3 = (int*)malloc(1000*sizeof(int));
    if (!arr3) { throw "cannot malloc arr3"; }
    ON_FINALLY({ free(arr3); });

    arr1[1] = 1;
    arr2[2] = 2;
    arr3[3] = 3;
}

All cleanup code is executed in reverse order (in the order opposite to the order of the ON_FINALLY/ON_EXCEPTION macros appearance in the function). The cleanup code is executed only if control passes beyond the corresponding ON_FINALLY/ON_EXCEPTION macro.

Check the following link to see the output of the demo program execution: http://coliru.stacked-crooked.com/a/d6defaed0949dcc8

Simsar answered 17/2, 2018 at 15:17 Comment(4)
You don't need ghastly macros for this. https://mcmap.net/q/24813/-the-simplest-and-neatest-c-11-scopeguard/560648Plater
Note that in the newer standards, std::uncaught_exceptions replaces std::uncaught_exceptionDefloration
@LightnessRacesinOrbit, I agree. But then I have to generate names for all my guards by myself. I'm too lazy to give the names for variables that I will never refer in my code.Simsar
@anton_rh: That's a pretty poor tradeoff in my view.Plater
D
0

C++ has destructors which is what you need. An object that does whatever you need done at scope exit in its destructor that you then create an instance of on the stack in the scope where you need the work done, will get destroyed when the scope is left and then do the work at that time.

Depurate answered 17/2, 2018 at 15:38 Comment(5)
This doesn't cover the case where you only want to do something in case of an exception being thrownDefloration
@JVApen: Thing is, that requirement is itself a massive code smell. There should be no distinction.Plater
@JVApen: However, it's easy to cancel the cleanup action when it's no longer needed. For example, std::unique_ptr<T> has a release() member function that results in not calling the deleter. Generally "exception vs non-exception" is the wrong criteria, you want "early exit vs normal exit", and release() works great for that. With C++11 and later move constructors, it's possible to accomplish transfer of cleanup responsibility to the caller with no extra application code at all.Smoothtongued
@LightnessRacesinOrbit, how about strong exception-safety guaranty? You need to rollback changes only if exception is thrown. Two different branches for exception and no-exception case.Simsar
Destructors are not necessarily the best solution for the rollback example. I'd prefer operating on a copy of the original data and explicitly swap the copy with the original at the end (IOW, the swap operation would commit the transaction). That doesn't involve custom destructors. (Of course, destructors are crucial for wrapping low-level resources with C-style release/free functions.)Emlyn
C
0

ScopeGuard is the right choice for you. It basically calls the function you specify at destructor.

So your code can be:

void your_function() {
  scope_guard guard = [&vector]() {
    vector.pop_back();
  };
  // your code
  guard.dismiss(); // success
}
Culley answered 23/6, 2019 at 10:20 Comment(1)
This would be a great answer, if there where a common general-purpose implementation. Seems like there is one in a TS, so we might have one soon...Hayes

© 2022 - 2024 — McMap. All rights reserved.