Hacker News new | past | comments | ask | show | jobs | submit login
Why RAII rocks (bromeon.ch)
40 points by jjuhl on April 23, 2015 | hide | past | favorite | 62 comments



Yes, of course RAII is much shorter than a contrived example designed to be as long as possible. Here's a much shorter version:

    int unsafe2()
    {
        A* a = new A;
        int retval = 0;
    
        if (a->f())
        {
            B* b = new B;
    
            if (b->f())
                retval = b->g();
            else
                retval = a->g();
    
            delete b;
        }
    
        delete a;
        return retval;
    }
I agree that RAII is a good thing, but can we please avoid straw-man examples?


Won't this leak A if B's constructor throws an exception?


Why not just put a and b on the stack then? ;)

Bjarne says, "Code that creates an object using new and then deletes it at the end of the same scope is ugly, error-prone, and inefficient."


Because it's an example of allocating things on the heap. You could also say the same about the "good" RAII version shown - but then there wouldn't be much point to the article, would there?


How does this version handle exceptions?


It doesn't. Just like the example it replaces.

I'm not saying this is the best way to write this code - there are several reasons RAII is better. But in this case, "look how messy non-RAII code is!" isn't the reason.


The original example does actually properly deal with exceptions.


This code work with assertion that constructors of A and B will never throw !


I think 3 responses here have misinterpreted what you're illustrating. (e.g. "won't that leak?!?!")

KeytarHero could have wrote: "Here's a much shorter more realistic version of new/delete code that will have problems such as leakage after exceptions"

Since he didn't make that explicit, 3 replies seem to have misinterpreted the post as: "Here's a much shorter version that won't leak and doesn't need RAII"

Basically, his example is supposed to be "wrong" but it's a shorter version of "wrong".


Exactly. Thanks.


Do you want to tell that this is correct and bug free code ? This is a example how to create lot of Memory Leaks.


It's certainly not bug free. But that's not the point. All I did was rewrite that example with half as much code. The point of that example seems to be "look how much less code RAII takes!" and I'm just trying to show that, although you should use RAII, that's not the reason why.


I was surprised how little I missed manual memory management when I switched from C++ to C# for everyday work.

RAII is something I do miss, though, even after years of C# I still feel lifetime management of objects is awkward.


For unmanaged resources using provides almost the same thing, the only downside I can see is it means yet more nesting.


You can forget to write a using statement. In C++ you can omit your destructor entirely if all the members are already RAII classes, whereas in C# you still need to write a .Dispose() method.

Memory management is by and far the vast majority of resource management in C++ for me, which C# takes care of with garbage collection - so I don't miss RAII too much in C#. But if you tried to give me a C++ with using blocks but no RAII (read: you gave me C) I'd scream.

RAII is no panacea[1] - I'd say C#'s manual resource burden without RAII is still less than C++'s with RAII. Which is why I don't miss it too much. But it'd still be a nice addition. And I missed it dearly during my initial adoption of C#.

[1] e.g. in C++, you still have to worry about reference cycles, implementing the RAII constructs in the first place if existing ones aren't suitable, ensuring parents are kept in scope while referencing children, etc..


> But if you tried to give me a C++ with using blocks but no RAII (read: you gave me C)

If you don't care about MSVC, IMHO new C code should be using the gcc cleanup extension: http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initial...


> If you don't care about MSVC

Even ignoring personal taste: I care about MSVC. I also care about clang. GCC is the one compiler I'm able to not care about.

> IMHO new C code should be using the gcc cleanup extension

Even if I didn't care about MSVC, I'd disagree.

I'm OK with extensions that are "harmless" in that the program will still run without them working if I #if them out on other compilers - error pragmas, deprecation annotations, static analysis hints, pre-C++11 override keywords, etc.

I'm not OK with self inflicting vendor lock-in for something as important as cleanup rely on a specific compiler's extensions - especially not when we have a perfectly standard, portable, significantly better tested (and thus less likely to have bugs) reasonable alternative in the form of C++ destructors.

If I'm not using C++ destructors, it's either because:

1) I'm doing small changes to an existing C project (in which case I'd be stylistically inconsistent with it's preferred cleanup patterns for minimal gain, since it almost certainly doesn't use the gcc cleanup extension)

or

2) because I can't rely on having decent C++ compilers on my target platforms (which means I can't rely on having GCC either, and thus can't rely on the gcc cleanup extension by definition.)


For what its worth, it is only MSVC without support. gcc compiler extensions are otherwise still rather portable. To clang and icc at least.


> For what its worth, it is only MSVC without support.

How recently? I've been enough versions back that clang documented pragmas haven't been available. The cleanup attribute... I can only find docs for GCC. Although I see LLVM bugs for it, so you're right about clang supporting it - at least on HEAD.

> gcc compiler extensions are otherwise still rather portable.

Portable or not, I'd say only about half the extensions I've sought out on clang have actually been available. If that.

> To clang and icc at least.

I should note in scenario #2 above I don't have these available either, since these are decent C++ compilers.


The other downside is you aren't as cleanly encapsulating the resource management logic.


I think the omission of deterministic destructors is one of the biggest mistakes the C# designers made. The Dispose pattern feels like a band aid.


Love me some RAII. Also, why is C++ chock full of such opaque acronyms? RAII and SFINAE come to mind.


It's old, and it was created and sharpened by old people with Unix smudges on their fingers. This is not so much a good answer, as an excuse to indulge myself and listen to Stephenson for a moment.

The file systems of Unix machines all have the same general structure. On your flimsy operating systems, you can create directories (folders) and give them names like Frodo or My Stuff and put them pretty much anywhere you like. But under Unix the highest level--the root--of the filesystem is always designated with the single character "/" and it always contains the same set of top-level directories:

/usr /etc /var /bin /proc /boot /home /root /sbin /dev /lib /tmp

and each of these directories typically has its own distinct structure of subdirectories. Note the obsessive use of abbreviations and avoidance of capital letters; this is a system invented by people to whom repetitive stress disorder is what black lung is to miners. Long names get worn down to three-letter nubbins, like stones smoothed by a river.


> Long names get worn down to three-letter nubbins, like stones smoothed by a river.

Except when they don't ;)


More: NRVO, Pimpl, SBRM, EBO, NVI, ODR, CRTP, STL, RTTI, TMP.

I've been using C++ for the past year at work. It's an absolute nightmare to learn.


SBRM, EBO, NVI, ODR, TMP -> care to explain those?

Anyway it does prove your point: have been using c++ way longer than you and these don't immediately ring a bell. Always more to learn! (or at least, always more super subtle details you'll only encounter once in a lifetime)


SBRM = Scope Bound Resource Management ( another name for RAII ) EBO = Empty Base Optimization NVI = Non-Virtual Interface ODR = One Definition Rule TMP = Template Meta Programming


Funny. I do know all those principles but had no idea there were common abbreviations.


A year? That's nothing. Personally I'd consider anyone with less than 5 years of professional C++ experience a newbie. It's a complex language, it takes years to learn it well.


Why would you heap allocate something that you're deleting in the same function? Isn't there some equivalent length example that isn't so contrived?


There are numerous cases when one would do exactly that, which all boil down to "this thing is too big for stack allocation." Ever done any image processing?


In addition, RAII is useful for more than just memory [de]allocation: database connections, resource handles, anything that you have to get/create and then cleanup/release.


As someone that works 90% with python and 10% with C++, that's something that gets missed in the debates about GC/manual memory management. It's a lot easier to leak resources in python programs because the whole point is you aren't sure what the lifetime is of some objects; if you knew, you wouldn't need a GC. So I end up putting a with block pretty high in the call stack holding the DB handles (most of the time) which is just a weaker form of RAII, as you can't "allocate" anything further down the call stack.

I prefer working in python to C++, I just wish I had a lot more control over things like this. And no, __del__ isn't a good solution as that opens a whole can of worms (for one, it's not guaranteed to ever be called).


What do you mean when you say 'as you can't "allocate" anything further down the call stack'? Do you mean you can't allocate something in c to be cleaned up in 'a', in the call tree:

    a(b(c()))
because just returning an object, then dropping the reference at the end of 'a' would do just that. You could also write your own context manager to wrap up a parameter.

    class Shadow(object):
        def __init__(self):
            self.value = None
        def __enter__(self):
            return self
        def set(self, v):
            self.value = v
        def __exit__(self, *args):
            if self.value is not None:
                self.value.cleanup()
        
    with Newdb() as db, Shadow() as d:
        a(b(c(db, d)))


you can do something like

    with a(b(c)) as f:
but then you risk any of those functions throwing an exception and screwing up the whole thing. You can move the resource allocation from __init__ to __enter__, but now you split your initialization code over 2 constructors. You can just hope and pray that a and b don't throw an exception, but someone is gonna accidentally break that assumption. And even if they don't, you've now spent more time worrying about that then if you just did manual memory allocation.

Or, in C++ you write

    class Foo {
    public:
        Foo() {/*get handle*/}
        ~Foo() {/*release resource*/}
    };
and you never have to worry again.


> You can move the resource allocation from __init__ to __enter__, but now you split your initialization code over 2 constructors

After giving it some thought, I've come up with a better example which more closely maps to C++ RAII constructs, which shows that you don't need to split anything. Consider:

    class Shadow(object):
        def __init__(self):
            # Initialization code
        @classmethod
        def __enter__(cls, *args, **kwargs):
            return cls(*args, **kwargs)
        def __exit__(self, *args, **kwargs):
            # De-initialize
`__enter__` becomes three lines of boilerplate, which you could abstract out into an inheritable class, should you so desire.

As for "In C++ you write", it's the same as saying "in Python you write __enter__ and __exit__", but instead of instantiating via `std::unique_ptr` you use `with`.


Huh, neat. I'd seen @classmethod a few times but wasn't sure what it did. I need to see if I can refactor some code now.


Python has context managers, which were added for exactly this purpose.

  with alloc_resource() as resource:
      resource.use()


I mentioned those. They work, but they're just a weak form of RAII without any extra power.


Can you elaborate on "extra power"? A c++ constructor/destructor pair is equivalent to __enter__ and __exit__. I fail to see how this grants a significant amount of power. There's certainly differences, but the gap is probably not as large as your weasel words make it out to be.

You complain about having to put initialization into 2 constructors, but there's the flip-side with things like mutexes. In c++, you have to have 2 classes, one for the lock and one for the guard. Whereas with a context-manager, you have one class and the locking code is in the __enter__ and __exit__.

    with self.lock:
        self.do_stuff()
In that case, I would say that the context-manager is nicer.


I didn't have any example of extra power, just in with blocks you have some downsides compared to RAII but no upsides. By downsides I mean that in C++ you write the constructor/destructor and never worry about it again, but in python every caller has to use a with block, plus what I said above about exceptions being thrown before __enter__.

I haven't done much with mutexes, but doesn't having it split over 2 classes introduce a race condition? Seems like a weird way to implement it to me.


Agree with @czinck. It works, it does the job, but it's not as fool proof as:

    {
        Type obj;
        // do stuff
    }
    // obj destructor just ran
C# and other languages have similar paradigms, but they require more attention to detail than a destructor does.


then maybe you will be interested in PHP - it has good RAII and I use it in one lib for resources control and mutexes.


Even in cases like that it's uncommon for the object to actually take up a lot of stack space. Since member variable's sizes have to be known at compile time it only applies to fixed-size (or parametrized) buffers.


As far as I know, using `new` or not has nothing to really do if it goes onto the stack or not. That's simply a common implementation details.

What its really saying is if the lifetime is automatic or manual.

Maybe I'm confusing C with C++ or just outright confused, but to my understanding, nothing says that:

    int foo[] = <massive amount of data>;
actually has to live on the stack.


Well, in practice, it's both. Sure, stacks and heaps are an implementation detail, but they're pervasive. You simply can't write int x[10000][10000]; and expect it to not cause a stack overflow in any real world implementation. It's an implementation detail you absolutely need to be aware of.


> nothing says that [...] actually has to live on the stack.

This might be possible in theory but I've never seen it in practice.


There's also such a thing as move constructors, so that you can pass ownership to somewhere else and have it be deleted there.

For instance,

  std::unique_ptr<X> f () {
    return std::make_unique<X>();
  }

  void h (std::unique_ptr<X> x) {
    x->doSomething();
    // x is released after this function returns
  }

  void g () {
    auto x = f();
    h(std::move(x));
  }


Isn't that similar to ARC in Objective-C?


ARC is (almost) purely automatic. You write your code however, and then a code analyzer goes through it to figure out where to put the memory management calls. (Also I'm pretty sure ARC still just does reference counting).


ARC stands for Automatic Reference Counting: http://en.wikipedia.org/wiki/Automatic_Reference_Counting


Essentially, yes.

In ARC, all strong pointers may have multiple owners. In C++11 RAII, the typical, defining use is for single owners (std::unique_ptr) but multiple owners are supported too (std::shared_ptr).

In ARC, local pointers may be reclaimed before their scope ends, if the compiler determines no further use of the pointer. In C++11 RAII, all such resources are reclaimed only at scope end.

ARC is not exception-safe by default, you have to compile with -fobjc-arc-exceptions to make it exception safe; however Apple standards have been moving away from using exceptions for recoverable conditions for some time now. C++11 RAII is of course exception-safe by default, it's one of the rationales behind the technique.


Welcome to 2015.


Does your language have mutexes and exceptions? You NEED RAII...


Rust is the answer. C++ is just soo legacy.


So what? Sure, rust is the new thing, node.js is the new thing, forget about SQL since NoSQL is the current thing. Etc etc, Bla bla bla. I hate this "let's chase every new thing and everything old is shit" attitude. C++ may be old but it does a great job at what it is targeted for (just like Fortran and many other "old" things). I'd really wish more people would stop chasing "new shiney" and just get work done. C++ may be complicated and old, but it gets work done.

Old, complicated and legacy does not equal bad.


It's also caused billions of dollars in damage. Hell, even just null pointers. Saying it does a great job is very generous. Even if it is the best language for low level work, it is still barely acceptable.

We should strive for advancement and progress in our field. There is a large difference between this and chasing the latest fad JS framework.

Unless you are saying that programming is a solved problem and C++ is about as advanced as we'll ever get.


And Rust is still going through its shakedown cruise. I wouldn't yet be comfortable taking it in the deep waters where C++ currently roams.

There's still a lot of need for tooling, testing, and letting people with smaller projects (and/or non-Mozilla use cases) shake out the bugs.

Give it a few successful years post 1.0, and Rust will be in a better place to truly take the mantle from C++.


It's sort of funny that you're holding rust to a far higher standard than c++ has ever met. A more honest post would say "c++ will continue to be used because c++ is what is used and that's how we like it."


The only standard that I'm holding C++ to is that it's been around longer, which means that the big holes have been patched (or bilge pumps dispatched), and developers who use it are realistic about what they are able to achieve.

That doesn't seem like a very high standard to hold Rust to. The problem is there's still holes we don't know about, and the developers who evangelize it are all starry-eyed about "never having to deal with memory again!"

When the shine wears off a bit (on both the language and its users), the big holes can be found and patched, the bilge pumps dispatched for the small ones, and Rust can move forward with a lot more confidence.


Agreed! I do not even use Rust myself (yet). I just do not think the comparison to the NoSQL/JS Frameworks craze is valid. There are real fundamental improvements being tried in Rust, hopefully it turns out for the better.


Rust is the answer to a different question.




Consider applying for YC's W25 batch! Applications are open till Nov 12.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: