Hacker News new | ask | show | jobs
by mort96 172 days ago
That approach doesn't allow you to move the file into some long lived object or return it in the happy path though, does it?
2 comments

As someone coming from RAII to C#, you get used to it, I'd say. You "just" have to think differently. Lean into records and immutable objects whenever you can and IDisposable interface ("using") when you can't. It's not perfect but neither is RAII. I'm on a learning path but I'd say I'm more productive in C# than I ever was in C++.
I agree with this. I don't dislike non-RAII languages (even though I do prefer RAII). I was mostly asking a rhetorical question to point out that it really isn't the same at all. As you say, it's not a RAII language, and you have to think differently than when using a RAII language with proper destructors.
Pondering - is there a language similar to C++ (whatever that means, it's huge, but I guess a sprinkle of don't pay for what you don't use and being compiled) which has no raw pointers and such (sacrificing C compatibility) but which is otherwise pretty similar to C++?
Rust is the only one I really know of. It's many things to many people, but to me as a C++ developer, it's a C++ with a better template model, better object lifetime semantics (destructive moves <3) and without all the cruft stemming from C compat and from the 40 years of evolution.

The biggest essential differences between Rust and C++ are probably the borrow checker (sometimes nice, sometimes just annoying, IMO) and the lack of class inheritance hierarchies. But both are RAII languages which compile to native code with a minimal runtime, both have a heavy emphasis on generic programming through templates, both have a "C-style syntax" with braces which makes Rust feel relatively familiar despite its ML influence.

You can move the burden of disposing to the caller (return the disposable object and let the caller put it in a using statement).

In addition, if the caller itself is a long-lived object it can remember the object and implement dispose itself by delegating. Then the user of the long-lived object can manage it.

> You can move the burden of disposing to the caller (return the disposable object and let the caller put it in a using statement).

That doesn't help. Not if the function that wants to return the disposable object in the happy path also wants to destroy the disposable object in the error path.

You have to write a disposable wrapper to return. Return it in error case too.

    readonly record struct Result<TResult, TDisposable>(TResult? IfHappy, TDisposable? Disposable): IDisposable where TDisposable : IDisposable
    {
        public void Dispose() => Disposable?.Dispose();
    }
Usage at call site:

    using (var result = foo.GetSomethingIfLucky())
    {
        if (result.IfHappy is {} success)
        {
            // do something
        }
    }