dimanche 3 juillet 2011

RAII and backtracking

A conventional wisdom is that C++ is a powerful language but difficult to use and that could be unsafe. In fact, this is wrong. Only some languages like C++ may lead to safe applications.
Why? This is mainly due to RAII. RAII means Resource Acquisition Is Initialization. You can have a look at Wikipedia.

Here is the idea. Consider a mechanism that is mainly implemented thanks to two opposite actions. For instance, you want to manage a file: you need to open and to close it. You want to manage resources: you will need to acquire and to release the resources. You need to manage the memory: you need to allocate and to deallocate the memory.

Now, imagine that a problem can happen in your program and you would like to be able to manage it. In other words, you have a normal code and a problem occurs. You need to catch it and to continue the program in a safe way. The main issue is that you need to abandon some current process before applying the second action of your mechanism. For instance, you need to close the files or release the resource you take. The main difficulty is that the error may happen anywhere in the program, so it is not easy to know what are the open files, the required resources... and how to access to this data.

C++ offers a nice way to deal with such a problem:  you can easily respect the RAII principle which is  "the object which performs the first action also performs the second one". In addition, the first action is performed in the constructor and the second one in the destructor. If a problem occurs then an exception is triggered and the execution goes back until a catch is found. The advantage of C++ is that all the destructors of the objects that are no longer valid will be called. Therefore, the files that have been opened will be closed, the resources will be release and so on... In addition, this will automatically be performed, so you don't have to be worried about it.

You can't do that in Java, because there is no real equivalent to destructor. There is no guarantee that the finalize function will be called and there is no guarantee about the order along with they will be called.

This is a strong advantage for C++.

C# has been recently modified for this reason.

Ok, but what is the relation with CP?

In CP we have to manage the wipeout event (the failure), that is when an inconsistent state is met. This happens when a domain is emptied for instance. In this case, the current state is inconsistent and we need to stop it immediately. Thus, we are face to the problem I explained. This problem involves all the local objects that have been defined or also the memory management.

There are usually 3 ways to deal with this in your code:
- You use C++ and exceptions. Unfortunately, exception is a slow mechanism
- You use C++ and setjmp/longjmp. Setjmp is faster than exception. However, it is not C++ compliant because the destructors are not called. This method is used in a lot of Solvers (like Gecode or or-tools), mainly because it is fast. Unfortunately this does not respect the RAII principle. You will have to manage by hand some part of the code.
ILOG Solver did not recommend to use destructor and mentioned that destructor would not be called for object allocated on the solver heap. Maybe, this is the reason...
- You deal with a lot of test. So, you add a lot of "if" in your code to make sure that there is no failure that happens. Unfortunately, you do not respect the RAII principles...

Hence, it seems that there is no perfect solution.
In fact, there is a fourth solution, better than the previous ones and almost perfect, but I keep it for me :-)

3 commentaires:

  1. Gecode does not use setjmp/longjmp. We use tests (but embedded in easy to use macros). This does respect RAII, btw (failure means the propagation function returns a failure code instead of success, and returning obviously destroys all stack allocated objects).

  2. Jean-CharlesRégin4 juillet 2011 à 07:38

    Thanks for the information
    So what did you use ? exception ? If you ask the user to check after each modification if there is a failure then it is equivalent to ask him to manage the 2 actions problem I mentioned.

  3. The user (i.e., someone who implements propagators, not the "end user" of the library) has to check, using a macro. E.g. in order to post x >= 3, you'd call CHECK(x.gq(home, 3)). The macro is essentially defined as
    #define CHECK(modification) if (failed(modification)) return FAILURE;
    It's not too intrusive, and all local objects are destroyed automatically when the function returns.