Does C++ support 'finally' blocks? (And what's this 'RAII' I keep hearing about?)

前端 未结 16 1422
情深已故
情深已故 2020-11-22 17:15

Does C++ support \'finally\' blocks?

What is the RAII idiom?

What is the difference between C++\'s RAII idiom and C#\'s \'using\' statement?

相关标签:
16条回答
  • 2020-11-22 17:34

    In C++ the finally is NOT required because of RAII.

    RAII moves the responsibility of exception safety from the user of the object to the designer (and implementer) of the object. I would argue this is the correct place as you then only need to get exception safety correct once (in the design/implementation). By using finally you need to get exception safety correct every time you use an object.

    Also IMO the code looks neater (see below).

    Example:

    A database object. To make sure the DB connection is used it must be opened and closed. By using RAII this can be done in the constructor/destructor.

    C++ Like RAII

    void someFunc()
    {
        DB    db("DBDesciptionString");
        // Use the db object.
    
    } // db goes out of scope and destructor closes the connection.
      // This happens even in the presence of exceptions.
    

    The use of RAII makes using a DB object correctly very easy. The DB object will correctly close itself by the use of a destructor no matter how we try and abuse it.

    Java Like Finally

    void someFunc()
    {
        DB      db = new DB("DBDesciptionString");
        try
        {
            // Use the db object.
        }
        finally
        {
            // Can not rely on finaliser.
            // So we must explicitly close the connection.
            try
            {
                db.close();
            }
            catch(Throwable e)
            {
               /* Ignore */
               // Make sure not to throw exception if one is already propagating.
            }
        }
    }
    

    When using finally the correct use of the object is delegated to the user of the object. i.e. It is the responsibility of the object user to correctly to explicitly close the DB connection. Now you could argue that this can be done in the finaliser, but resources may have limited availability or other constraints and thus you generally do want to control the release of the object and not rely on the non deterministic behavior of the garbage collector.

    Also this is a simple example.
    When you have multiple resources that need to be released the code can get complicated.

    A more detailed analysis can be found here: http://accu.org/index.php/journals/236

    0 讨论(0)
  • 2020-11-22 17:34

    why is it that even managed languages provide a finally-block despite resources being deallocated automatically by the garbage collector anyway?

    Actually, languages based on Garbage collectors need "finally" more. A garbage collector does not destroy your objects in a timely manner, so it can not be relied upon to clean up non-memory related issues correctly.

    In terms of dynamically-allocated data, many would argue that you should be using smart-pointers.

    However...

    RAII moves the responsibility of exception safety from the user of the object to the designer

    Sadly this is its own downfall. Old C programming habits die hard. When you're using a library written in C or a very C style, RAII won't have been used. Short of re-writing the entire API front-end, that's just what you have to work with. Then the lack of "finally" really bites.

    0 讨论(0)
  • 2020-11-22 17:36

    No, C++ does not support 'finally' blocks. The reason is that C++ instead supports RAII: "Resource Acquisition Is Initialization" -- a poor name for a really useful concept.

    The idea is that an object's destructor is responsible for freeing resources. When the object has automatic storage duration, the object's destructor will be called when the block in which it was created exits -- even when that block is exited in the presence of an exception. Here is Bjarne Stroustrup's explanation of the topic.

    A common use for RAII is locking a mutex:

    // A class with implements RAII
    class lock
    {
        mutex &m_;
    
    public:
        lock(mutex &m)
          : m_(m)
        {
            m.acquire();
        }
        ~lock()
        {
            m_.release();
        }
    };
    
    // A class which uses 'mutex' and 'lock' objects
    class foo
    {
        mutex mutex_; // mutex for locking 'foo' object
    public:
        void bar()
        {
            lock scopeLock(mutex_); // lock object.
    
            foobar(); // an operation which may throw an exception
    
            // scopeLock will be destructed even if an exception
            // occurs, which will release the mutex and allow
            // other functions to lock the object and run.
        }
    };
    

    RAII also simplifies using objects as members of other classes. When the owning class' is destructed, the resource managed by the RAII class gets released because the destructor for the RAII-managed class gets called as a result. This means that when you use RAII for all members in a class that manage resources, you can get away with using a very simple, maybe even the default, destructor for the owner class since it doesn't need to manually manage its member resource lifetimes. (Thanks to Mike B for pointing this out.)

    For those familliar with C# or VB.NET, you may recognize that RAII is similar to .NET deterministic destruction using IDisposable and 'using' statements. Indeed, the two methods are very similar. The main difference is that RAII will deterministically release any type of resource -- including memory. When implementing IDisposable in .NET (even the .NET language C++/CLI), resources will be deterministically released except for memory. In .NET, memory is not deterministically released; memory is only released during garbage collection cycles.

     

    † Some people believe that "Destruction is Resource Relinquishment" is a more accurate name for the RAII idiom.

    0 讨论(0)
  • 2020-11-22 17:37

    FWIW, Microsoft Visual C++ does support try,finally and it has historically been used in MFC apps as a method of catching serious exceptions that would otherwise result in a crash. For example;

    int CMyApp::Run() 
    {
        __try
        {
            int i = CWinApp::Run();
            m_Exitok = MAGIC_EXIT_NO;
            return i;
        }
        __finally
        {
            if (m_Exitok != MAGIC_EXIT_NO)
                FaultHandler();
        }
    }
    

    I've used this in the past to do things like save backups of open files prior to exit. Certain JIT debugging settings will break this mechanism though.

    0 讨论(0)
  • 2020-11-22 17:42

    Not really, but you can emulate them to some extend, for example:

    int * array = new int[10000000];
    try {
      // Some code that can throw exceptions
      // ...
      throw std::exception();
      // ...
    } catch (...) {
      // The finally-block (if an exception is thrown)
      delete[] array;
      // re-throw the exception.
      throw; 
    }
    // The finally-block (if no exception was thrown)
    delete[] array;
    

    Note that the finally-block might itself throw an exception before the original exception is re-thrown, thereby discarding the original exception. This is the exact same behavior as in a Java finally-block. Also, you cannot use return inside the try&catch blocks.

    0 讨论(0)
  • 2020-11-22 17:44

    I came up with a finally macro that can be used almost like¹ the finally keyword in Java; it makes use of std::exception_ptr and friends, lambda functions and std::promise, so it requires C++11 or above; it also makes use of the compound statement expression GCC extension, which is also supported by clang.

    WARNING: an earlier version of this answer used a different implementation of the concept with many more limitations.

    First, let's define a helper class.

    #include <future>
    
    template <typename Fun>
    class FinallyHelper {
        template <typename T> struct TypeWrapper {};
        using Return = typename std::result_of<Fun()>::type;
    
    public:    
        FinallyHelper(Fun body) {
            try {
                execute(TypeWrapper<Return>(), body);
            }
            catch(...) {
                m_promise.set_exception(std::current_exception());
            }
        }
    
        Return get() {
            return m_promise.get_future().get();
        }
    
    private:
        template <typename T>
        void execute(T, Fun body) {
            m_promise.set_value(body());
        }
    
        void execute(TypeWrapper<void>, Fun body) {
            body();
        }
    
        std::promise<Return> m_promise;
    };
    
    template <typename Fun>
    FinallyHelper<Fun> make_finally_helper(Fun body) {
        return FinallyHelper<Fun>(body);
    }
    

    Then there's the actual macro.

    #define try_with_finally for(auto __finally_helper = make_finally_helper([&] { try 
    #define finally });                         \
            true;                               \
            ({return __finally_helper.get();})) \
    /***/
    

    It can be used like this:

    void test() {
        try_with_finally {
            raise_exception();
        }    
    
        catch(const my_exception1&) {
            /*...*/
        }
    
        catch(const my_exception2&) {
            /*...*/
        }
    
        finally {
            clean_it_all_up();
        }    
    }
    

    The use of std::promise makes it very easy to implement, but it probably also introduces quite a bit of unneeded overhead which could be avoided by reimplementing only the needed functionalities from std::promise.


    ¹ CAVEAT: there are a few things that don't work quite like the java version of finally. Off the top of my head:

    1. it's not possible to break from an outer loop with the break statement from within the try and catch()'s blocks, since they live within a lambda function;
    2. there must be at least one catch() block after the try: it's a C++ requirement;
    3. if the function has a return value other than void but there's no return within the try and catch()'s blocks, compilation will fail because the finally macro will expand to code that will want to return a void. This could be, err, avoided by having a finally_noreturn macro of sorts.

    All in all, I don't know if I'd ever use this stuff myself, but it was fun playing with it. :)

    0 讨论(0)
提交回复
热议问题