When has RAII an advantage over GC?
Asked Answered
T

8

14

Consider this simple class that demonstrates RAII in C++ (From the top of my head):

class X {
public:
    X() {
      fp = fopen("whatever", "r");
      if (fp == NULL) 
        throw some_exception();
    }

    ~X() {
        if (fclose(fp) != 0){
            // An error.  Now what?
        }
    }
private:
    FILE *fp;
    X(X const&) = delete;
    X(X&&) = delete;
    X& operator=(X const&) = delete;
    X& operator=(X&&) = delete;
}

I can't throw an exception in the destructor. I m having an error, but no way to report it. And this example is quite generic: I can do this not only with files, but also with e.g posix threads, graphical resources, ... I note how e.g. the wikipedia RAII page sweeps the whole issue under the rug: http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

It seems to me that RAII is only usefull if the destruction is guaranteed to happen without error. The only resources known to me with this property is memory. Now it seems to me that e.g. Boehm pretty convincingly debunks the idea of manual memory management is a good idea in any common situation, so where is the advantage in the C++ way of using RAII, ever?

Yes, I know GC is a bit heretic in the C++ world ;-)

Tedesco answered 3/1, 2012 at 12:58 Comment(4)
Don't you have the same issue in a finally-section in, e.g., Java?Cambridgeshire
one of example that comes (not very relevant to your example but ...) to mind are lock guards: boost.org/doc/libs/1_48_0/doc/html/thread/…Centreboard
When misusing garbage collectors for general resource management (like file handles), the exact same problem occurs. Where should the garbage collector throw exceptions? The interested code is long gone.Foreplay
Just for the sake of completeness: Your example is missing copy ctor and assignment operator.Getty
P
14

RAII, unlike GC, is deterministic. You will know exactly when a resource will be released, as opposed to "sometime in the future it's going to be released", depending on when the GC decides it needs to run again.

Now on to the actual problem you seem to have. This discussion came up in the Lounge<C++> chat room a while ago about what you should do if the destructor of a RAII object might fail.

The conclusion was that the best way would be provide a specific close(), destroy(), or similar member function that gets called by the destructor but can also be called before that, if you want to circumvent the "exception during stack unwinding" problem. It would then set a flag that would stop it from being called in the destructor. std::(i|o)fstream for example does exactly that - it closes the file in its destructor, but also provides a close() method.

Polyethylene answered 3/1, 2012 at 13:7 Comment(12)
This seems the most realistic answer to my question as of yet. In RT applications I would agree with you. However, most standard applications can perfectly live with a slowdown of a few milliseconds once in a while.Tedesco
@user844382: what milliseconds? The delay before an object is GCed could be hours, if the application isn't allocating resources. There's no "slowdown" - with mark-sweep GC the application carries on running, and a finalizer may or may not execute some time later.Floruit
@user: Exactly what Steve says. If there is no need to free the resource space, the GC will never even consider to release your resource (except if explicitly told so by GC.Collect() for example).Polyethylene
@user844382: Xeo has a far more important point. Determinism is not about the speed advantage, but about the reproducibility of errors. Have a Java application that leaks memory? You'll never find the pointer loop or the GC bug that triggered it because the GC does its thing irreproducibly. Have a non-GC C++ application with the same problem? Run it once under valgrind and you got the bug.Foreplay
@Foreplay That's not really true. It's trivial to find memory "leaks" in java applications. Make snapshot of heap after some time, let application run some time, make new snapshot, compare. Voila you'll easily find the hashmap (the usual candidate) or whatever structure that keeps the references. valgrind is nice but sadly doesn't work for all applications (and not just anything windows, hotspot itself has the tendency to overburden valgrind as well). And about hotspot bugs: Well it's a c application, so you better not claim it's impossible to find memory leaks in c/c++ ;)Germany
The thing is though: GC is there to manage memory NOT system resources. With a few hacks it can be made to do so (ie run all finalizers at shutdown), but it wasn't made for it and shouldn't be used in such a way. I don't want to run a GC every time a system resource gets scant in the vague hope that a finalizer will release some things (and not to speak of the slowdown for the whole GC when using lots of finalizers)Germany
@Steve Jessop But RAII can also end up running several hours later or never, if the object is owned by a shared_ptr, or if the control flow is such that the object leaves scope. Any object which is not in scope or which has no references to it (i.e. cases where scope or shared_ptr would call the destructor) will always be freed by a GC on a full run. A frequently running GC will pretty much always clear long-lived objects faster than RAII-based methods.Herrah
@saolof: Leaving aside reference loops, RAII destructs the object at precisely the moment that it becomes eligible for garbage-collection. GC cannot free it any earlier than that. Accounting for detached reference loops, OK, GC might free the object "next time it runs", which is sooner than what RAII does: free it "never". But this answer is about the determinism of RAII, and the fact remains that RAII is deterministic. Probably not what you intended, but deterministically so.Floruit
@Steve Jessop "RAII destructs the object at precisely the moment that it becomes eligible for garbage-collection." No it doesn't. You can have something like void foo() {hugetype x = makeHugeObject(); bar(x); doLongOperation(); } Where x can be reclaimed before or during the doLongOperation bit. With RAII, x will unnecessarily be around for the whole doLongOperation computation and consume memory. This gets particularly bad in recursive algorithms where RAII can leak while GC would not. It's also possible to design a GC that will always cleanup earlier than RAII in every single case.Herrah
@saolof: I'm a bit rusty on this, but I think in C++ x is not eligible for garbage collection during doLongOperation(). Bear in mind that destroying it might have observable side-effects, which the implementation can't re-order relative to doLongOperation(). Obviously this is kind of a circular definition: RAII destroys the object precisely when eligible for destruction, because by design RAII plays off the C++ object lifetime rules, which say the object is eligible for destruction the end of the scope where RAII was used. But it's the definition we have.Floruit
And of course if there's been enough inlining to prove no observable difference, either RAII or GC is equally entitled to use the "as-if" rule to destroy x early. Both could deduce that at the same certain point we can treat x as out of scope since it's never used again.Floruit
A GC collects garbage at runtime. It can collect at any time while doLongOperation() is running. It does not need to know anything about scopes, and it does not care about inlining. If doLongOperation() is big enough that at least 1 gc collection is very likely while it is running, then GC will almost always clean that up faster. While RAII has to wait until the scope of the object ends before it can collect, which can be an arbitrarily long time, and which will generally block tail call optimization.Herrah
P
13

This is a straw man argument, because you're not talking about garbage collection (memory deallocation), you're talking about general resource management.

If you misused a garbage collector to close files this way, then you'd have the identical situation: you also could not throw an exception. The same options would be open to you: ignoring the error, or, much better, logging it.

Piano answered 3/1, 2012 at 13:5 Comment(2)
You are correct when you say that GC doesn't solve the non-memory case either, and my question could be phrased better. Trying to rephrase it a little better: RAII isn't any better at resource management than GC, and it requires you to write (and especially: debug) a lot of code. So when would you actually be better of whit RAII?Tedesco
May be the B.Stroustup's argument why GC is not yet implemented in the standard will partially answer the question - see "Evolving a language in and for the real world: C++ 1991-2006", ch. 5.4 "Automatic Garbage Collection" (www2.research.att.com/~bs/hopl-almost-final.pdf)Ilona
F
7

The exact same problem occurs in garbage collection.

However, it's worth noting that if there is no bug in your code nor in the library code which powers your code, deletion of a resource shall never fail. delete never fails unless you corrupted your heap. This is the same story for every resource. Failure to destroy a resource is an application-terminating crash, not a pleasant "handle me" exception.

Flail answered 3/1, 2012 at 13:5 Comment(9)
So it would be acceptable to print out an error and exit? Or better ignore it (possibly after logging)?Cambridgeshire
Your second paragraph is a bit over the top IMO, as I can imagine plenty of reasons why general resource deallocations might fail that aren't end-of-the-world scenarios. If a disk fills up and you can't close a file, you notify the user and try again later.Piano
I don't think this is always true. For example failure to delete a temp file should not necessarily lead to application termination.Radically
@NiklasBaumstark Ignore it (yes, possibly after logging). fclose always closes the file, even if it fails to write all data that had not yet been written to disk, so resource wise, there is no error.Abet
@hvd: But admittedly, there is an error application-wise. The caller expects that his data is written and if we force him to flush before, we could as well force him to close and don't use RAII at all.Cambridgeshire
@NiklasBaumstark It's not the same thing: fclose should be called even if there's an exception halfway during writing of the data. But if there's an exception halfway writing the data, you don't really care whether fclose succeeds, because the data is wrong anyway.Abet
@hvd: Ah, that makes sense. So the difference is that flush does not have to be called to leave in a consistent state, while fclose has to be called. Thanks.Cambridgeshire
@hvd Ignore it is not acceptable. Imagine writing a text, saving it, and finding out the last few kilobytes are not stored. Resource-wise no error, but i would love my program to tell me this before.Tedesco
@user844382 That's why a separate flush() method can throw an exception.Abet
F
4

First: you can't really do anything useful with the error if your file object is GCed, and fails to close the FILE*. So the two are equivalent as far as that goes.

Second, the "correct" pattern is as follows:

class X{
    FILE *fp;
  public:
    X(){
      fp=fopen("whatever","r");
      if(fp==NULL) throw some_exception();
    }
    ~X(){
        try {
            close();
        } catch (const FileError &) {
            // perhaps log, or do nothing
        }
    }
    void close() {
        if (fp != 0) {
            if(fclose(fp)!=0){
               // may need to handle EAGAIN and EINTR, otherwise
               throw FileError();
            }
            fp = 0;
        }
    }
};

Usage:

X x;
// do stuff involving x that might throw
x.close(); // also might throw, but if not then the file is successfully closed

If "do stuff" throws, then it pretty much doesn't matter whether the file handle is closed successfully or not. The operation has failed, so the file is normally useless anyway. Someone higher up the call chain might know what to do about that, depending how the file is used - perhaps it should be deleted, perhaps left alone in its partially-written state. Whatever they do, they must be aware that in addition to the error described by the exception they see, it's possible that the file buffer wasn't flushed.

RAII is used here for managing resources. The file gets closed no matter what. But RAII is not used for detecting whether an operation has succeeded - if you want to do that then you call x.close(). GC is also not used for detecting whether an operation has succeeded, so the two are equal on that count.

You get a similar situation whenever you use RAII in a context where you're defining some kind of transaction -- RAII can roll back an open transaction on an exception, but assuming all goes OK, the programmer must explicitly commit the transaction.

The answer to your question -- the advantage of RAII, and the reason you end up flushing or closing file objects in finally clauses in Java, is that sometimes you want the resource to be cleaned up (as far as it can be) immediately on exit from the scope, so that the next bit of code knows that it has already happened. Mark-sweep GC doesn't guarantee that.

Floruit answered 3/1, 2012 at 13:12 Comment(3)
As a practical solution, I agree. However, you are actually proving my point: The destructor here does not help the programmer. Yes the file is closed, or the mutex destroyed, but when something goes wrong, you have silent file corruption, a strange deadlock, etc... I rather have my app crashing a this point.Tedesco
@user844382: I thought you were asking a question: your blog is for making points. The destructor does help the programmer, it ensures that fclose is called on the FILE*. It doesn't do everything else that the programmer might want to do. If your point was that GC does do everything (even that it does everything RAII does), then your point is simply incorrect. If you truly want the app to crash, then you can call abort or dereference a null pointer instead of logging at the point I indicate.Floruit
Oh, and the file corruption is only "silent" if you incorrectly assume that the file is written without successfully calling close. So don't do that, any more than you would assume the file is written without first calling fwrite to actually write some data. Regarding mutexes - I don't know under what circumstances releasing your mutexes is failing, but I would suggest fixing the surrounding code, since the only documented failures of (e.g) pthread_mutex_unlock are that the input isn't a valid mutex or isn't owned by the caller.Floruit
F
4

Exceptions in destructors are never useful for one simple reason: Destructors destruct objects that the running code doesn't need anymore. Any error that happens during their deallocation can be safely handled in a context-agnostic way, like logging, displaying to the user, ignoring or calling std::terminate. The surrounding code doesn't care because it doesn't need the object anymore. Therefore, you don't need to propagate an exception through the stack and abort the current computation.

In your example, fp could be safely pushed into a global queue of non-closeable files and handled later. The calling code can continue without problems.

By this argument, destructors very rarely have to throw. In practice, they really do rarely throw, explaining the widespread use of RAII.

Foreplay answered 3/1, 2012 at 13:13 Comment(1)
The purpose of a destructor isn't to destroy things that aren't needed any more. The purpose is to make any resources an object was using (memory, files, GDI handles, or whatever) available for future use by other objects. If a routine has an automatic variable Foo of class type Bar, that variable will cease to exist when the routine exits, without the destructor having to do anything. The purpose of the destructor is not to change something that's going to cease to exist, but rather to manipulate outside entities that should be usable without the destroyed object.Hoff
U
4

I want to chip in a few more thoughts relating to "RAII" vs. GC. The aspects of using some sort of a close, destroy, finish, whatever function are already explained as is the aspect of deterministic resource release. There are, at least, two more important facilities which are enabled by using destructors and, thus, keeping track of resources in a programmer controlled fashion:

  1. In the RAII world it is possible to have a stale pointer, i.e. a pointer which points to an already destroyed object. What sounds like a Bad Thing actually enables related objects to be located in close proximity in memory. Even if they don't fit onto the same cache-line they would, at least, fit into the memory page. To some extend closer proximity could be achieved by a compacting garbage collector, as well, but in the C++ world this comes naturally and is determined already at compile-time.
  2. Although typically memory is just allocated and released using operators new and delete it is possible to allocate memory e.g. from a pool and arrange for an even compacter memory use of objects known to be related. This can also be used to place objects into dedicated memory areas, e.g. shared memory or other address ranges for special hardware.

Although these uses don't necessarily use RAII techniques directly, they are enabled by the more explicit control over memory. That said, there are also memory uses where garbage collection has a clear advantage e.g. when passing objects between multiple threads. In an ideal world both techniques would be available and C++ is taking some steps to support garbage collection (sometimes referred to as "litter collection" to emphasize that it is trying to give an infinite memory view of the system, i.e. collected objects aren't destroyed but their memory location is reused). The discussions so far don't follow the route taken by C++/CLI of using two different kinds of references and pointers.

Urease answered 3/1, 2012 at 19:38 Comment(0)
T
2

Q. When has RAII an advantage over GC?

A. In all the cases where destruction errors are not interesting (i.e. you don't have an effective way to handle those anyway).

Note that even with garbage collection, you'd have to run the 'dispose' (close,release whatever) action manually, so you can just improve the RIIA pattern in the very same way:

class X{
    FILE *fp;
    X(){
      fp=fopen("whatever","r");
      if(fp==NULL) throw some_exception();
    }

    void close()
    {
        if (!fp)
            return;
        if(fclose(fp)!=0){
            throw some_exception();
        }
        fp = 0;
    }

    ~X(){
        if (fp)
        {
            if(fclose(fp)!=0){
                //An error. You're screwed, just throw or std::terminate
            }
        }
    }
}
Thoroughgoing answered 3/1, 2012 at 13:7 Comment(7)
This is almost what I was going to post: add a flush() method. If the fclose in the destructor fails, no, you're not screwed. The file is closed, but some data might not have been saved. If the caller wanted an exception for that, the caller should have called flush() first.Abet
Your first sentence is not a sentence.Cambridgeshire
@hvd: well, I meant you're screwed in general as that is clearly the OP's intent. For this particular destructor, yes you are right.Thoroughgoing
@NiklasBaumstark: well thank you for reporting that. It makes a lot of sense in response to the question, though.Thoroughgoing
@Thoroughgoing It applies to all destructors. If your class holds resources, it can release them without an exception, also in those other cases mentioned in the question.Abet
@hvd: no, not necessarily safely. Yes, you can swallow exceptions, if you will. I'd certainly not recommend that. (Example: Failing to release a semaphore or mutex should be grounds to terminate operation, or UB ensues, potentially breaking thread synchronisation or resulting in deadlock).Thoroughgoing
@Thoroughgoing Your class should keep track of whether the mutex is valid, and check that in the destructor. If it is valid, then releasing the mutex cannot fail. This is guaranteed in pthreads, and likely in other implementations as well.Abet
U
1

Destructors are assumed to be always success. Why not just make sure that fclose won't fail?

You can always do fflush or some other things manually and check error to make sure that fclose will succeed later.

Underact answered 3/1, 2012 at 12:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.