6

RAII is by far the most useful idiom in c++. However there seem to be two common pitfalls associated with it that I believe need to be formally addressed.

Failure to release a resource in the destructor and resource invalidation prior to deconstruction. What are your thoughts on solid pattern extensions to incorporate these pitfalls?

Examples of resource release failure:

  1. Failing to close a file
  2. Failing to shutdown a socket connection

Examples of resource invalidation (dangling resource reference):

  1. The socket peer closed the connection
  2. The resource has been revoked by the third party provider

Here is a rough new RAII object that I designed to handle the resource invalidation problem utilizing the feedback so far (feedback always welcome):

template<typename T> class shared_weak_ptr;

template<typename T>
class revoke_ptr {
  friend class shared_weak_ptr<T>;
public:
  typedef std::function<void()> revoke_func;
  revoke_ptr(T* t) : _self(std::make_shared<model_t>(t)) {}

  void revoke() {
    if (!_self->_resource) throw 1; // Already revoked
    for (auto func : _self->_revoke_funcs) func();
    _self->_revoke_funcs.clear();
    _self->_resource.reset();
  }
private:
  struct model_t {
    model_t(T* t) : _resource(t) {}
    std::shared_ptr<T> _resource;
    std::list<revoke_func> _revoke_funcs;
  };

  std::shared_ptr<model_t> _self;
};

template<typename T>
class shared_weak_ptr {
public:
  template<typename R>
  shared_weak_ptr(revoke_ptr<T> revoke_ptr, R revoke_callback) : _self(std::make_shared<const model_t>(std::move(revoke_ptr), std::move(revoke_callback))) {}

  std::shared_ptr<T> lock() const { return _self->_revoke_ptr._self->_resource; }
private:
  using revoke_func = typename revoke_ptr<T>::revoke_func;
  using revoke_it = typename std::list<revoke_func>::iterator;
  struct model_t {
    model_t(revoke_ptr<T> revoke_ptr, revoke_func revoke_callback) : _revoke_ptr(std::move(revoke_ptr)) {
      if (!_revoke_ptr._self->_resource) throw 1; // The resource has already been revoked
      _revoke_ptr._self->_revoke_funcs.emplace_back(std::move(revoke_callback));
      _revoke_it = _revoke_ptr._self->_revoke_funcs.end();
      --_revoke_it;
    }
    ~model_t() {
      if (!_revoke_ptr._self->_resource) return; // Already revoked so our callback has already been removed
      _revoke_ptr._self->_revoke_funcs.erase(_revoke_it); // Remove our callback
    }
    revoke_ptr<T> _revoke_ptr;
    revoke_it _revoke_it;
  };

  std::shared_ptr<const model_t> _self;
};

Example Usage:

int main() {
  revoke_ptr<int> my_revoke_ptr(new int(5));

  typedef std::shared_ptr<const uint8_t> unique_key;
  unique_key key = std::make_shared<const uint8_t>();

  std::map<unique_key, shared_weak_ptr<int>> my_collection;

  shared_weak_ptr<int> my_shared_weak_ptr(my_revoke_ptr, [key, &my_collection](){
    std::cout << "revoked" << std::endl;
    auto it = my_collection.find(key);
    if (it != my_collection.end()) {
      my_collection.erase(it);
    }
  });
  my_collection.emplace(key, my_shared_weak_ptr);

  if (auto ptr = my_shared_weak_ptr.lock()) {
    assert(true);
    std::cout << *ptr << std::endl;
  } else {
    assert(false);
    std::cout << "nope" << std::endl;
  }

  my_revoke_ptr.revoke();

  if (auto ptr = my_shared_weak_ptr.lock()) {
    assert(false);
    std::cout << *ptr << std::endl;
  } else {
    assert(true);
    std::cout << "nope" << std::endl;
  }

  assert(my_collection.find(key) == my_collection.end());
  return 0;
}
  • Either of those is a case of being able to lead a horse to water but not being able to make him drink. – Blrfl Nov 22 '16 at 03:22
  • 1
    Both these issues basically just come down to: "RAII wasn't actually used (whether it was intended to be or not)." – Jerry Coffin Nov 22 '16 at 04:26
  • 1
    @JerryCoffin There are several examples of RAII being "used" to ensure that things such as file descriptors are closed. However these examples rarely discuss what happens when that close fails. I am simply asking for elegant ways to handle these situations. – Aaron Albers Nov 22 '16 at 05:18
  • 1
    @AaronAlbers: And what exactly does it mean if the file's closure fails? Is it still open? Even if `fclose` returns an error, the `FILE` object is no longer valid. – Nicol Bolas Nov 22 '16 at 15:22
  • 1
    @NicolBolas Closing a file can sometimes fail because it implicitly flushes to disk and the flush can fail. Ignoring this error could lead to unintended data lose. – Aaron Albers Nov 22 '16 at 17:32
  • @AaronAlbers: But the fact that you closed the file *without* checking for that possibility means that you have chosen not to care about what happens in that case (which is true of lots of applications). Thus, there's a difference between manually closing a file and letting RAII handle it. – Nicol Bolas Nov 22 '16 at 17:53
  • @NicolBolas I would normally agree but RAII is also a handy tool when it comes to managing shared ownership. Therefore you don't want to close a file until it is no longer being used and the best way to find out is when the destructor is called. – Aaron Albers Nov 22 '16 at 17:58
  • 2
    @AaronAlbers: If you have a shared resource, you don't even know if calling a destructor will release the resource. So you are very much *not* in a position to actually handle any errors caused by any failure of releasing that resource. – Nicol Bolas Nov 22 '16 at 18:03
  • Let us [continue this discussion in chat](http://chat.stackexchange.com/rooms/48956/discussion-between-aaron-albers-and-nicol-bolas). – Aaron Albers Nov 22 '16 at 18:06

4 Answers4

4

You might consider throwing some exception from the destructor (but that is frown upon for good reasons, and if you do that, you really need to be careful....; your comment cites this). But then the real question is what would you do next?

You need to think about what do to on such kind of failures. In many cases, I would believe that failing a close(2) or a shutdown(2) is so unusual and serious that you practically can just emit a message (e.g. on some log file, perhaps using syslog(3)....) and terminate the program. Of course, YMMV. In some cases you need to do something clever on close failure, but what you'll do is application and domain specific.

(I am not sure to fully understand what happens inside the Linux kernel when close is failing, and I am not sure that kernel resources are released properly before termination of process in that weird case; details are system and file-system specific.)

You might also create your "socket" object with a closure inside it, or some callback, to handle errors.

There is no universal answer to your concerns; it is specific to a class of applications, a domain, your operating system, your particular computer configuration -both hardware & software- etc... BTW, your question is not RAII or C++ specific (similar concerns would hold for Ocaml code which does not use RAII, or even for C code).

Look also (in particular if you are concerned by critical embedded systems) at MISRA C, the future MISRA C++ (and the old Embedded C++), and industrial standards like DO-178C. Find the one relevant to your domain and application areas (or else, take inspiration from some domains or application areas which are similar to yours). Look also into Common Criteria, read more about Formal methods. But there is no silver bullets!

Read also about leaky abstractions (in practice, you cannot avoid them) and Joel's law of leaky abstractions. Don't expect language features or programming paradigms to solve or even guide all software design issues.

Basile Starynkevitch
  • 32,434
  • 6
  • 84
  • 125
  • 1
    Everywhere I look I always see that throwing an exception from a destructor is [bad](http://stackoverflow.com/questions/130117/throwing-exceptions-out-of-a-destructor). And terminating a program is not always an acceptable option especially in [Life-Critical Systems](https://en.wikipedia.org/wiki/Life-critical_system). I normally would supply a callback on construction that can be called if an error occurs during destruction. – Aaron Albers Nov 22 '16 at 06:45
  • I agree with you, but you need to be a lot more specific and pragmatical. There cannot be any universal answer, you should handle such errors wisely and on a case by case basis. Regarding life critical systems, you should follow the standards of your industry. – Basile Starynkevitch Nov 22 '16 at 07:03
2

I would say that the best way to handle this situation is to let the user handle it.

Consider a file, encapsulated by a RAII object. If the destructor closes the file, then the user is saying that they do not care if the closure of the file provokes an error.

They way they would express caring about it is by manually closing it, via an explicit close function. That way, they can detect errors and do something about them.

This distinction is important, particularly in the case of throwing exceptions. Let's say you open a file, do some operation, then close it (manually). If that "some operation" part throws an exception that gets caught outside of your scope, what exactly do you intend to do if stack unwinding provokes an error on the file's closure? You're in the middle of leaving that scope; you aren't exactly in a position to fix any data that gets mangled as a result of this error. Indeed, the exception that's causing this stack unwinding may be because of the file's corruption, so you may not even be able to correct it.

So if you're wrapping some resource in a RAII object, and the release of that resource could provoke an error (which does not represent the continued existence of that resource), I would say that the most reasonable way to handle that is to provide a manual way to release that resource early. One which can fail and can do so in easily detectable/handle-able ways.


One example I gave was a socket being closed by the remote peer.

RAII, ultimately, represents ownership. That is, it represents who has control over whether that resource still exists.

shared_ptr models shared ownership of a dynamically-allocated C++ object; it will continue to exist so long as someone still holds a shared_ptr to it. weak_ptr models weak-sharing; it may or may not exist, but if it doesn't exist anymore, you can at least ask if it isn't there.

If a socket can be "closed" by someone who isn't you, then you don't have any real ownership of it. Your relationship to the socket is more like a weak_ptr than a shared_ptr.

Your object type should model the relationship you have to that object.

Nicol Bolas
  • 11,813
  • 4
  • 37
  • 46
  • Every answer so far has only dealt with the resource failing to release. Does anyone have suggestions or comments on when the resource becomes invalid? – Aaron Albers Nov 22 '16 at 18:10
  • I'm not sure what the difference between "fails to release" and "becomes invalid" is. For example, `fclose` *always* successfully closes the file handle. The only errors that may happen are that the data within that file may not be entirely what you wrote. The suggestion I made above only makes sense if the resource is genuinely released (ie: no leaks); for a resource which may fail to be released, the destructor has to ensure that this does not happen. Whatever it has to do in order to do that, it must do. – Nicol Bolas Nov 22 '16 at 18:38
  • One example I gave was a socket being closed by the remote peer. You are holding onto this object because you wish the socket to remain open but it was closed against your wishes (of course there are many other examples that fit that same pattern). – Aaron Albers Nov 22 '16 at 18:44
  • @AaronAlbers: Then you should design your resource object accordingly. This is no different from what happens if you have a pointer that someone else deletes. How you choose to handle that is up to you and your specific needs. And the answer ultimately has little to do with RAII. – Nicol Bolas Nov 22 '16 at 18:54
  • Agreed that is has little to do with RAII as it is. That is why I want to discuss extensions to RAII to incorporate these scenarios in which RAII alone cannot be used but commonly is. – Aaron Albers Nov 22 '16 at 18:59
  • 1
    Then your question is confused. How a particular type models an ownership relationship is up to you. RAII is the tool used to model a relationship, but the meaning of that relationship and how it is expressed is up to you. `unique_ptr` represents unique ownership, not because it uses RAII, but because it cannot be copied. `shared_ptr` represents shared ownership because it has mechanisms for allowing multiple instances to own the same memory. These are not "extensions to RAII"; they are *uses* of RAII. A house is not an extension of a nail, but you need nails to build a house. – Nicol Bolas Nov 22 '16 at 19:02
  • Thanks for clearing that up. Now I know that I am looking to create a new templated type that encompasses a solution for these common problems. – Aaron Albers Nov 22 '16 at 19:44
  • @AaronAlbers: "*Now I know that I am looking to create a new templated type that encompasses a solution for these common problems.*" Well, you first have to figure out what those relationships actually are. Simply saying that a resource can be released at any time by someone else is not enough information to actually find an appropriate solution. You have to get into the details of the ownership relationship you want to have, which depends in part on how you want the user to use the system. – Nicol Bolas Nov 22 '16 at 20:49
1

What are your thoughts on solid pattern extensions to incorporate these pitfalls?

resource release failure

  • In the general, simplistic case, ignore all errors in destructors. (ignore error returns, catch+eat exceptions): I do find this very unattractive, but I also find this to be the only sane way given the language as it stands.
  • If logging is available, then DO log these errors.
  • For specific cases, or specific classes, having throwing destructors that signal these errors through an exception is OK and totally fine - see below for known restrictions and thoughts.

resource invalidation (dangling resource reference)

I think this solidly falls out of the scope of what we can handle via RAII. Well put in other answer:

RAII, ultimately, represents ownership. That is, it represents who has control over whether that resource still exists.

If a socket can be "closed" by someone who isn't you, then you don't have any real ownership of it. Your relationship to the socket is more like a weak_ptr than a shared_ptr.


The conceptual problem is that the destructor ends the lifetime of the object unconditionally, and if the object was solely responsible for the resource, then really, you can't fail, the resource owner will always be gone.

I'll just quote others:

... The real reason is that an exception should be thrown if and only if a function's postconditions cannot be met. The postcondition of a destructor is that the object no longer exists. This can't not happen. Any failure-prone end-of-life operation must therefore be called as a separate method before the object goes out of scope (sensible functions normally only have one success path anyway)

Of course, as others have noted that may often be too simplistic and kind of defeats the purpose of one aspect of RAII, namely that you can't forget to close/flush/whatever.

I've had my thoughts on this as well and it's not like a complete solution, but I think it's helpful:

The whole problem becomes easier to think about when we split classes in two types. A class dtor can have two different responsibilities:

  • (R) release semantics (aka free that memory)
  • (C) commit semantics (aka flush file to disk)

If we view the question this way, then I think that it can be argued that (R) semantics should never cause an exception from a dtor as there is a) nothing we can do about it and b) many free-resource operations do not even provide for error checking, e.g. void free(void* p);.

Objects with (C) semantics, like a file object that needs to successfully flush it's data or a ("scope guarded") database connection that does a commit in the dtor are of a different kind: We can do something about the error (on the application level) and we really should not continue as if nothing happened.

Reading up on this, there are several technical hurdles to throwing destructors:

  • arrays and containers - no luck: objects living in there never must throw while living in there
  • dynamic deallocation - seems this still doesn't work (353 isn't in the standard as far as I can see)
  • Throwing d'tor while already doing exception unwinding (the classic case) - this is actually partially solved in C++14/17, see std::uncaught_exception*s* and its rationale

I think the std::uncaught_exceptionS feature will finally allow to easier handle the (IMHO very classic) case of file-close-fails-because-flush-fails-and-now-data-is-corrupt:

The classic advice is to call flush (or close) explicitly, as that way you can always meaningfully throw an error, which is unfortunately reverting back to manual resource-closing, which we would want to try to avoid!

With the uncaught_exceptionS machinery, we can finally write classes that only throw from their d'tor when they are "allowed" to do so, that is during regular scope exit:

I think this could be quite useful:

  • On the regular code path, failure to close(flush) the important-data-file will cause an exception, thereby cancelling the operation that wrote to the file, thereby allowing to report (maybe handle) the failure case directly without any manual-call-flush-and-close contortions.
  • On the exception-unwinding-code-path, the possible close-failure will simply be ignored (maybe logged), as the operation is already "cancelled" via an exception anyway, so probably (hopefully) the operation that required the file to be written/closed is considered a failure anyway, because of another error.

I know there's quite some "if"s here, but still I think it's worth a look if you're looking for "thoughts on solid pattern extensions to incorporate these pitfalls".

Martin Ba
  • 7,578
  • 7
  • 34
  • 56
0

Failure to release a resource in the destructor and resource invalidation prior to deconstruction. What are your thoughts on solid pattern extensions to incorporate these pitfalls?

I think you misunderstand what RAII is; To put it shortly (too much has been written about RAII already) RAII means make your acts of responsibility acquisition combine with the construction of a C++ object instance and release the resource in destruction).

This has nothing to do with the failibility of the act of releasing resources.

If you look at the concept of preconditions and postconditions, you can see there is an entire host of hardware and software states that simply cannot be tested at the level of software ("faulty network connector only works when the edge of the door pushes the cable towards the wall and the connector is held at an angle").

The existence of these conditions ensure you will have leaky abstractions (see Basile's answer): error conditions you cannot check for, and API calls that "cannot fail" but sometimes, they still do.

In these cases you have a few solutions: either specify at the API documentation level that there are conditions you do not check for/errors you do not handle, or ignore those cases (as situations your software cannot handle), or add an additional layer of checks/abstractions - such as handing release responsibility to a queue which will do the release with it's own checks and balances in place - and which can fail).

None of the actions proposed in the paragraph above are part of RAII, nor should they be, because RAII is not about how absolute the abstractions behind your API are.

utnapistim
  • 5,285
  • 16
  • 25