3

I've recently begun learning C++, previously I programmed in Go.

I was recently informed that I should not be using new because exceptions thrown may cause the allocated memory not to be freed and result in a memory leak. One popular solution to this is RAII, and I found a really good explanation of why to use RAII and what it is here.

However, coming from Go this whole RAII thing seemed unnecessarily complicated. Go has something called defer that solves this problem in a very intuitive way. You just wrap what you want to do when the scope ends in defer(), e.g. defer(free(ptr)) or defer(close_file(f)) and it'll automagically happen at the end of the scope.

I did a search and found two sources that had attempted to implement the defer functionality in C++ here and here. Both ended up with almost exactly the same code, perhaps one of them copied the other. Here they are:

Defer implentation 1:

template <typename F>
struct privDefer {
    F f;
    privDefer(F f) : f(f) {}
    ~privDefer() { f(); }
};

template <typename F>
privDefer<F> defer_func(F f) {
    return privDefer<F>(f);
}

#define DEFER_1(x, y) x##y
#define DEFER_2(x, y) DEFER_1(x, y)
#define DEFER_3(x)    DEFER_2(x, __COUNTER__)
#define defer(code)   auto DEFER_3(_defer_) = defer_func([&](){code;})

Defer implementation 2:

template <typename F>
struct ScopeExit {
    ScopeExit(F f) : f(f) {}
    ~ScopeExit() { f(); }
    F f;
};

template <typename F>
ScopeExit<F> MakeScopeExit(F f) {
    return ScopeExit<F>(f);
};

#define SCOPE_EXIT(code) \
    auto STRING_JOIN2(scope_exit_, __LINE__) = MakeScopeExit([=](){code;})

I have 2 questions:

  1. It seems to me that this defer is essentially doing the same thing as RAII, but much neater and more intuitively. What is the difference, and do you see any problems with using these defer implementations instead?

  2. I don't really understand what the #define part does on these implementations above. What is the difference between the two and is one of them more preferable?

Alasdair
  • 13,348
  • 18
  • 82
  • 138
  • 5
    I fail to see how this is neater. With RAII, I am not required to tell an object to be deferred. It just handles things when it goes out of scope. Unless there's something else about defer that isn't being stated here. – sweenish Mar 17 '21 at 15:14
  • The scopeguard technique was invented in C++ a long long time ago as a very practical way to use RAII for exactly the purpose you're using defer for. As C++ has developed it is gotten lighter and better syntax too. I personally would use a bare scopeguard that wrapped a lambda - with the additional lambda syntax of `[]()` - rather than either of those two implementations with macros, but maybe that's just me. – davidbak Mar 17 '21 at 15:15
  • Plus, @sweenish is correct, now that I reread your question. _Neither_ of your examples - free or close - needs a scope guard/defer _at all_!! Instead, pointers and native resources should be wrapped in _classes_ with _destructors_ and then it is all entirely automatic. Thanks sweenish for pointing that out! Scopeguard/defer is for those use cases that are _left over_ after classes w/ destructors, and there aren't that many. – davidbak Mar 17 '21 at 15:16
  • These two macros seem to do totally same thing. And yes they are a good solution for you to emulate Go's `defer()`. In D language there is also same thing like `scope(exit) free(p);`. – Arty Mar 17 '21 at 15:17
  • @davidbak, I don't want to spend all my time writing classes with destructors, it's verbose and creates zillions of classes. – Alasdair Mar 17 '21 at 15:18
  • 4
    @Alasdair - Oh, ok. rather than 'spending all your time' writing classes with destructors you'd rather write code for _each use of each instance_. Gosh, I guess that's better. Can you tell me how it works when you want to protect one of these things in a situation where you're passing it from _one scope to another_? Because it would be nifty if _that_ worked, since you need to do it all the time! I prefer the classes / destructor trade off myself: You can't _forget_ to cleanup. – davidbak Mar 17 '21 at 15:19
  • 3
    I would feel confident that `std::unique_ptr` and Standard Library containers/functions would reduce the amount of classes you'd need to write by quite a bit. And if it's **not** heap allocated, you don't have to worry at all. – sweenish Mar 17 '21 at 15:20
  • 1
    Yes, and those very same pointers _handle both_ the free and the close use cases you used as examples! (You get to specify the `Deleter`). – davidbak Mar 17 '21 at 15:21
  • One more (more idiomatic?) approach to `defer`-like stuff is `gsl::finally` (https://github.com/Microsoft/GSL) – R2RT Mar 17 '21 at 15:21
  • BTW, notice that your two solutions use different lambda default capture, first uses `[&]` second uses `[=]`. Probably for the scoped case you don't need `[=]` and should use just `[&]` like in first solution, also not all objects allow copying and copying may cause some problem too. – Arty Mar 17 '21 at 15:22
  • 1
    As already noted, the difference between destructor and defer/finally is that the destructor only has to be written once for each class and the compiler does the work of adding cleanup to every function using the class. – Ben Voigt Mar 17 '21 at 15:23
  • @davidbak actually the one scope to another thing works fine because it's still destroying within the same scope it was created, same as with RAII. – Alasdair Mar 17 '21 at 15:33
  • Not if you intend to pass ownership. – davidbak Mar 17 '21 at 15:35
  • C++ has ownership? Are you sure? I don't look at C++ like that at all. Are you conflating the usage of certain libraries with what C++ is? – Alasdair Mar 17 '21 at 15:48
  • @Alasdair Well, what's your definition of ownership? See e.g. https://stackoverflow.com/questions/13852710/single-vs-shared-ownership-meaning or https://www.stroustrup.com/resource-model.pdf – Bob__ Mar 17 '21 at 16:02
  • 1
    AFAIK go's defer is executed at the end of a function block, not the current scope. C++ destructors will always run at the end of the current scope. So that's a difference for sure. – Timo Mar 17 '21 at 16:03
  • Those are abstractions that implement ownership functionality. There's no law that says they have to be used. That's not true in Rust for example, that has ownership as part of the language specification. – Alasdair Mar 17 '21 at 16:06
  • @Timo that's useful information, thanks! So this C++ defer implementation also runs at the end of the scope and not function block, right? – Alasdair Mar 17 '21 at 16:07
  • 1
    @Alasdair yes. Also note that this macro might break the ODR rule if it's used in inline functions. That would then result in a rather cryptic compiler error. – Timo Mar 17 '21 at 16:11

2 Answers2

9

A lot of what you're talking about is opinion-based, so I'm going to start with my own opinions.

In the C++ world, we expect RAII. If you want to get along well with other developers, you're both going to encounter it, and you're going to buck the standard if you decide to do something in a different fashion just because it's what you're accustomed to from Go.

Furthermore, C++ developers don't use FOPEN :-). The C++ standard library includes perfectly good RAII-enabled classes, and we use them. So having to implement RAII really means making proper choices of existing standard classes where possible or making sure your objects are RAII-compatible.

I pretty much never have to redesign my code to implement RAII. My choice of classes handles it automatically.

So while the code you've shown is interesting, it's actually more work than RAII. Every time you use FOPEN, you have to also remember to do your defer thing. Isn't it just so much easier to use std::ifstream or std::ofstream? Then it's already handled for you. (And this can be said about other times where your code would have to implement RAII on the spot. It's already done by picking the right classes.)

So, no, it's not neater and more intuitive, because you have to remember to do it. Pick the right classes, and you don't have to remember.

As for the #defines -- they're just there to make sure your variables have unique names and to shortcut the constructor of the defer class.

Joseph Larson
  • 8,530
  • 1
  • 19
  • 36
  • 1
    Plus real code does things like pass an instance _and its ownership_ from scope to scope - defer doesn't work for that, proper RAII does. – davidbak Mar 17 '21 at 15:29
  • 1
    Plus an advantage of 'you don't have to remember' is that you can _test once_ that cleanup happens, and then rely on it actually happening! You don't have to test each _use_.... – davidbak Mar 17 '21 at 15:30
  • Hmmm... I'm not convinced. (1) I don't work with other developers, (2) I already expect to do this since I'm *used* to doing it in Go. Hence it will be easier than learning a new standard for the same thing. What I wanted to ask was whether there's a problem with it. – Alasdair Mar 17 '21 at 15:37
  • 2
    Hey, I'm a C++ developer and I use `fopen()` -- partially because I'm stubborn, but mostly because I don't like the way the `ifstream`/`ofstream` APIs work. But when I do, I immediately hand the `FILE *` pointer to an RAII-style object whose destructor will `fclose()` it for me. – Jeremy Friesner Mar 17 '21 at 15:54
  • 1
    My whole purpose of learning C++ was to get away from having layers of abstraction and everything hidden beneath classes, which is what happens with Go or Rust. I want to be close to the system. Otherwise I'd just use Go. I could use C but C++ is basically C with frills, some of those frills are useful. – Alasdair Mar 17 '21 at 16:00
  • 3
    @Alasdair - you seem to have strong opinions about what you like and don't like. There's nothing wrong with that, but it can hold you back when you try to shift gears and move to a different environment - a different computer language, or library, or whatever. Each has different strengths and weaknesses and philosophies and _attitudes_. C++ has been evolving for over 30 years - faster, more recently - and it has benefited from the experience of building tens of thousands of large (and small, and embedded) systems over that time. My advice: Learn C++, don't just code Go using C++ syntax. – davidbak Mar 17 '21 at 16:57
  • 2
    @Alasdair - oh, and to answer your question from several comments above: There's _nothing wrong_ with using C++ this way. C++ is _multi-paradigm_ and can certainly support coders who prefer to code in some other language's idioms with C++ syntax. Other C++ programmers won't understand why you're doing it the hard way, but, since you don't work with other developers, who cares? Party on! (I'm serious here, not snarky. Do what works best for _you_, as long as it is only you it affects.) – davidbak Mar 17 '21 at 17:06
  • @Alasdair -- use the right classes to start with and you almost never have to think about it. – Joseph Larson Mar 17 '21 at 20:02
4

It seems to me that this defer is essentially doing the same thing as RAII, but much neater and more intuitively. What is the difference, and do you see any problems with using these defer implementations instead?

RAII's pro:

  • More secure and DRY: RAII avoid to use defer each time you acquire a resource.
  • RAII handles transfer-ownership (with move semantic).
  • defer can be implemented with RAII, not the other way (with movable resources).
  • With RAII, you might handle different paths for success/error (database commit/rollback in case of exception for example) (you might have finally/on_success/on_failure).
  • Can be composed (You might have object, with several resources).
  • Can be used at global scope. (even if global should be avoided in general).

RAII's cons:

  • You need one class by "resource type". (standard provides several generic one though, containers, smart pointers, lockers, ...).
  • Destructor code should not throw. (go doesn't have exception, but error handling with defer is also problematic).
  • Can be misused at global scope. (Static order initialization Fiasco SIOF).

For real resources, you should really use RAII.

For code where you have to rollback/delay a change, using a finally class might be appropriate. using MACRO in C++ should be avoided, so I strongly suggest to use the RAII syntax instead of the MACRO way

// ..
++s[i];
const auto _ = finally([&](){ --s[i]; })
backstrack_algo(s, /*..*/);

I don't really understand what the #define part does on these implementations above. What is the difference between the two and is one of them more preferable?

Both use same technique and use an object to do the RAII. so the macro (#define) is to declare an "unique" identifier (of the type of their object) to be able to call defer several time in the same function, so after MACRO replacement, it result to something like:

auto scope_exit_42 = MakeScopeExit([&](){ fclose(f);});

One use __COUNTER__, which is not a standard MACRO, but supported by most compiler (and so really ensure uniqueness). the other use ___LINE__, which is standard MACRO, but would break unicity if you call defer twice on the same line.

Other difference is the default capture which might be [&] (be reference, instead of by value) as lambda stay in scope, so no lifetime issue.

Both forget to handle/delete copy/move of their type (but as the variable cannot really be reused when using the macro).

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Great explanation, thank you. One question: let's say this is used for freeing memory behind a pointer, doesn't the defer macro avoid wrapping the pointer itself in a class container? In which case it would have faster access because I can use the pointer directly? It wraps only the deallocation function. (This is what I care about most: performance.) – Alasdair Mar 17 '21 at 16:55
  • I'll answer my own question: just read that the compiler should optimize it to be the same speed. Without optimizations unique_ptr would be slower. – Alasdair Mar 17 '21 at 17:01
  • 1
    `std::unique_ptr` has no overheads (`std::shared_ptr` has, but allows reference counting), compiler might easily inline code of RAII (dematerialize the object), to place the deallocation to the correct places. – Jarod42 Mar 17 '21 at 17:02
  • In addition, no reasons to have different performance between traditional RAII and RAII implemented `defer`. – Jarod42 Mar 17 '21 at 17:07
  • To be clear, it only doesn't have overhead because of compiler optimizations. That's not obvious, and it should be clear because otherwise people might think that classes don't have overheads... which they may or may not do depending on how they're compiled. – Alasdair Mar 17 '21 at 17:08
  • Without any optimization, you have just (trivial) extra function calls: `unique_ptr(T*)` and `~unique_ptr()`. As any function call in fact. (there are no memory indirection involved if it is your concern). – Jarod42 Mar 17 '21 at 17:28