14 ms·
To better understand the importance of RAII, you might consider what some other languages offer to solve similar problems. Consider a C example: int f()
by detrino 13y ago
To better understand the importance of RAII, you might consider what some other languages offer to solve similar problems.
Consider a C example:
int f()
{
int ret = -1;
handle * a = acquire_handle(0);
if (a == NULL) goto fail1;
handle * b = acquire_handle(1);
if (b == NULL) goto fail2;
handle * c = acquire_handle(2);
if (c == NULL) goto fail3;
// use a, b, c
ret = 0;
release_handle(c);
fail3:
release_handle(b);
fail2:
release_handle(a);
fail1:
return ret;
}
Consider a C# example:
void f()
{
using (handle a = new handle(0))
using (handle b = new handle(1))
using (handle c = new handle(2))
{
// use a, b, c
}
}
Now a C++ example using RAII:
void f()
{
handle a{0};
handle b{1};
handle c{2};
// use a, b, c
}
These examples are mostly equivalent (Although the C#/C++ assume exceptions instead of error codes).
The C#/C++ examples are far more structured and less error prone than the C example.
The advantage of C++'s RAII over C#'s using statement is that cleanup is tied to the object rather than a statement. This means that RAII is both composable and non-leaky as an abstraction. You cannot forget to destruct an object in C++, and you don't have to care that its destructor frees resources. When you have an IDisposable member in C# you must manually mark your class as IDisposable and then implement the Dispose method yourself. Clients must also be aware that your class is IDisposable in order to use it correctly.
- voidlogic 13y agoGo: func f() error { var a, b, c *handle var err error if a, err = acquireHandle(0); err != nil { return err } defer a.Close() if a, err = acquireHandle(1); err != nil { return err } defer b.Close() if a, err = acquireHandle(2); err != nil { return err } defer c.Close() //use a, b, c return nil }
- detrino 13y agoThanks for the example, seems like Go's solution is better than C but has the same problems as C#: non composable and leaky.
- masklinn 13y agoIt's significantly worse actually, as the acquire call does not imply the dispose. Disposal has to be invoked separately, even if close to acquire.
- ygra 13y agoStylistic side note for C#: If you're nesting using blocks like that you can leave out the braces in all but the deepest instance which reads a little nicer, imho: using (handle a = new handle(0)) using (handle b = new handle(1)) using (handle c = new handle(2)) { // use a, b, c }
- detrino 13y agoThanks for that, updated the example.
- dkuntz2 13y agoThat actually looks terrible to me, but I don't program in C#. Mostly it's because there are three blocks, but it only looks like there's one.
- ygra 13y agoIt gets rid of excessive nesting when you need multiple resources allocated after another (e.g. SqlConnection, SqlCommand, etc.). You can do using (handle a = new handle(0), b = new handle(1), c = handle(3)) { // use a, b, c } instead, too, but that obviously only works with equal types. I'm usually not a friend of too deep nesting (worst thing I've seen in our codebase was 65 spaces deep) and in C# you already have one level for the class, one for the namespace (possibly) and another for the method. No need to add two more if you can help it.
- dkuntz2 13y agoRight, I understand that it removes excessive nesting, but the first example just looked ... wrong to me. Your example with above looks somewhat better.
- dllthomas 13y agoIt's perhaps worth knowing, although it will hopefully never be relevant, that at least in GCC a longjmp will not run destructors.
- penguindev 13y agoThe worst problem is that the destructor should not throw an exception (AFAIK). Thus, c++ RIAA is not perfect - can't use it for things that could fail. edit: some people use it for closing/flushing files, bad idea. edit 2: I judge a language in part by seeing if they actually can define nested / chained exceptions well. If they don't, they're probably not too serious about exception handling...
- Suncho 13y agoOut of curiosity, how would you recommend handling a situation in which a file close fails? My instinct is to have an RAII class whose destructor catches any exceptions, adds the error to some list somewhere, and doesn't rethrow anything. What's an example of a language you like that does exceptions in a better way?
- penguindev 13y agoLike everyone, I'm still waiting for the perfect language. That said, due to this issue and the fact that writing exception safe code in c++ scares me (due to having datastructures potentially being in a wierd state), I have never used exceptions in c++. I'd like to try c#, to see if it has the power of pythons unchecked exceptions and with blocks but with more helpful static typing for more 'mission critical' stuff, but I'm quite torn because as my name suggests, I like working on linux...,
- penguindev 13y agoBy the way, I posted on the GOLang list a while ago my suggestion for error handling. It's like exception handling, but it's all done through function calls, (no try catch and jumping forward). Example: ret, e = _some_function() # e is non-null if an exception/error happened ret = some_function() # on error, an exception is raised and caught by the first parent function that used _call() syntax (leading underscore) So basically, _func() means call func(), and have an extra return value that is an exception or Null if no error. All one has to implement is either a _func or func, not both, the compiler handles the conversion. So bottom line, you can write real exception safe code and not have to worry about a panic/exception breaking the flow, by using _func() calls, or you can bail by using func() calls, it's your choice. edit: not sure how this ties in to RAII, if at all. Guess I should get back to the drawing board... :-)
- detrino 13y agoPerhaps its more fair to C to consider handle to be an opaque type like C#/C++. In that case the acquire block can be rewritten: handle a, b, c; if (!aquire_handle(&a, 0)) goto fail1; if (!aquire_handle(&b, 1)) goto fail2; if (!aquire_handle(&c, 2)) goto fail3;