Skip to content

Introduces a CGAL_destructor_assertion macro.#451

Merged
sloriot merged 6 commits intoCGAL:masterfrom
GilesBathgate:throwing-destructors
Jan 14, 2016
Merged

Introduces a CGAL_destructor_assertion macro.#451
sloriot merged 6 commits intoCGAL:masterfrom
GilesBathgate:throwing-destructors

Conversation

@GilesBathgate
Copy link
Copy Markdown
Contributor

The problem: Throwing an exception from a destructor can cause problems since the callee is unable to catch an exception raised while the stack is unwinding. The result is the application will terminate with a hard crash.

Consequence: Since the exception is uncatchable, the program will terminate, but also the real exception is hidden by the the destructor exception.

Solution: Its desirable to not throw exceptions from the destructor if and only if an existing exception has not been caught, and the stack is unwinding. For this purpose I propose a new macro that can safely be called from a destructor, even when the stack is currently unwinding. The macro makes use of std::uncaught_exception() and results in a nop when this is the case.

Additional discussions about this issue and what problems it can cause for client applications (rapcad and openscad) is detailed here: openscad/openscad#410, there was also a discussion about the issue here: http://cgal-discuss.949826.n4.nabble.com/Uncatchable-exception-converting-from-Nef-polyhedron-to-Polyhedron-3-td4658603.html

Another instance of the same problem present in the openscad project: openscad/openscad#1455

@lrineau
Copy link
Copy Markdown
Member

lrineau commented Nov 20, 2015

@GilesBathgate Could you please rebase your branch from cgal/master?

@sloriot I find the patch good and useful. Could you also have a look?

TODO: I think that however the pull-request should also modify the documentation. The file to modify is Documentation/doc/Documentation/Developer_manual/Chapter_checks.txt

@gdamiand
Copy link
Copy Markdown
Member

Instead of disabling totally the macro, could it be better to display a message about the problem when std::uncaught_exception() ?

@GilesBathgate
Copy link
Copy Markdown
Contributor Author

@gdamiand Although this does disable the macro from throwing an exception when the stack is unwinding, the uncaught exception still propagates, and so you still get an exception message which can be handled gracefully. If you are saying that the uncaught exception should be caught within the macro, some additional information added to it, and then rethrown. I guess that might work.

@gdamiand
Copy link
Copy Markdown
Member

Sorry, but I don't understand how this works.

If I am in a destructor and I use the new macro CGAL_destructor_assertion(EX), then std::uncaught_exception() is true ?

If yes, then the CGAL_destructor_assertion macro is disabled and no assertion are raised.

If no, assertion is raised like previously.

@GilesBathgate
Copy link
Copy Markdown
Contributor Author

@gdamiand There are two scenarios.

Scenario A: No exceptions were thrown by the class, but the assertion in the destructor is false. In this case it is fine to throw an exception from the destructor.

Scenario B: The class threw an exception in a method. As part of the exception clean-up routines the program unwinds the stack, and deletes local variables as it does so. When it tries to delete the class that also fails the assertion in its destructor, it is now in a situation where the current exception has not been caught, (and there is no chance of catching it the destructor either because it exists on different stack frame). At this point if you throw another exception the program terminates, (via std::terminate())

I admit its a little tricky to grasp,I found this article quite useful: http://www.kolpackov.net/projects/c++/eh/dtor-1.xhtml

The example given here http://en.cppreference.com/w/cpp/error/uncaught_exception also illustrates the scenarios I described above.

@GilesBathgate
Copy link
Copy Markdown
Contributor Author

@gdamiand Here is a demonstration program, the program demonstrates Scenario A and that part works as expected. But in Scenario B it should also print "Exited program cleanly" but it doesn't because of the uncaught exception (uncommenting the line in the destructor will fix the problem)

#include <iostream>
#include <exception>
#include <stdexcept>

class Foo {
int check_protocoll;
public:
    Foo() : check_protocoll(0){}
    ~Foo() {
        // if(!std::uncaught_exception())
             if(check_protocoll) throw std::runtime_error("Destructor assertion failed");
    }

    void success()
    {
        check_protocoll=1;
    }

    void fail() {
       check_protocoll=1;
       throw std::runtime_error("Assertion failed");
    }
};

int main()
{
    // Scenario A
    try {
        Foo f;
        f.success();
    } catch (const std::exception& e) {
        std::cout << "Exception caught: " << e.what() << '\n';
    }
    // Scenario B
    try {
        Foo f;
        f.fail();
    } catch (const std::exception& e) {
        std::cout << "Exception caught: " << e.what() << '\n';
    }
    std::cout << "Exited program cleanly\n";
}

OUTPUT

Exception caught: Destructor assertion failed
terminate called after throwing an instance of 'std::runtime_error'
  what():  Destructor assertion failed
Aborted (core dumped)

Output when line uncommented

Exception caught: Destructor assertion failed
Exception caught: Assertion failed
Exited program cleanly

@gdamiand
Copy link
Copy Markdown
Member

Ok, I understand now. Thank you very much for the detailed answer.

@GilesBathgate
Copy link
Copy Markdown
Contributor Author

It may also be worth noting that when the above program is compiled for c++11, even Scenario A terminates the program. This is because in c++11 you have to explicitly mark destructor's with noexcept(false), if they are expected to throw exceptions.

@GilesBathgate
Copy link
Copy Markdown
Contributor Author

@lrineau I propose an additional macro CGAL_NOEXCEPT(false), which will only be defined when CGAL_CXX11 is defined. This is to take care of the situation in my previous comment. Should I put that in another merge request or add it to this one?

@lrineau
Copy link
Copy Markdown
Member

lrineau commented Nov 23, 2015

@GilesBathgate You can add it to this pull-request. It should also be documented in the developers manual.

This macro can safely be called from a destructor, even when
the stack is currently unwinding, and thus prevents uncatchable
exceptions.
This macro is available for future compatibility with c++11, which
by default marks destructors noexcept(true). Some destructors
in CGAL do throw exceptions an so should be marked noexcept(false).
Since noexcept is not available in c++0x and below the macro is
disabled when CGAL_CXX11 is not defined since it is not required.
@GilesBathgate
Copy link
Copy Markdown
Contributor Author

@lrineau I am trying to put together some notes for the documentation. This is a first draft, but I am not an excellent technical documenter.

...

  • Destructor assertions These are checks which can be made within normal clean up of an object. A special macro is provided to ensure these checks are not made when the object is being cleaned up during exception handling.

The according macro names all have the format CGAL_<check_type> where <check_type> can be one of

  • precondition
  • postcondition
  • assertion
  • warning
  • static_assertion
  • destructor_assertion

...

Exception handling

Any destructor which might throw an exception, including a destructor which uses the destructor_assertion macro, should be marked with the CGAL_NOEXCEPT(false) macro. This macro provides future compatibility with C++11 and above which provides the noexcept keyword.

@lrineau
Copy link
Copy Markdown
Member

lrineau commented Dec 2, 2015

@GilesBathgate, that looks good. I suggest a small modification: explicit the name of the special macro.

A special macro CGAL_destructor_assertionis provided to ensure...

Provide information about the CGAL_destructor_assertion, and the
CGAL_NOEXCEPT macros.
@GilesBathgate
Copy link
Copy Markdown
Contributor Author

@lrineau I have updated the documentation in this merge request now. Is there anything else you would like doing before lifting the TODO flag?

@lrineau lrineau removed the TODO label Dec 3, 2015
@lrineau
Copy link
Copy Markdown
Member

lrineau commented Dec 3, 2015

@GilesBathgate Nothing, as far as I can see. It looks good.

@sloriot will now merge your pull-request in our testsuite branch, to test if it can be integrated in cgal/master.

@sloriot
Copy link
Copy Markdown
Member

sloriot commented Dec 4, 2015

Merged in CGAL-4.8-Ic-65

@lrineau
Copy link
Copy Markdown
Member

lrineau commented Dec 14, 2015

Unfortunately, for the moment that web site is password-protected. We plan to migrate to a Docker container for it, and then we will open it to public.

@mglisse
Copy link
Copy Markdown
Member

mglisse commented Dec 16, 2015

Why not CGAL_NOEXCEPT(CGAL_NO_ASSERTIONS), to avoid penalizing release builds?

@GilesBathgate
Copy link
Copy Markdown
Contributor Author

@mglisse Sounds sensible, I assume you mean to amend calls to the macro, rather than the macro itself?

@lrineau Shall I push this change also, or does this interfere with the current test cycle?

@lrineau lrineau removed the Tested label Dec 17, 2015
@lrineau
Copy link
Copy Markdown
Member

lrineau commented Dec 17, 2015

@GilesBathgate You can push a modification. It will be retested.

This avoids penalizing release builds in which assertions are not
made within the destructors, and so no exception can be thrown.
@sloriot
Copy link
Copy Markdown
Member

sloriot commented Jan 4, 2016

Merged in CGAL-4.8-Ic-91

@sloriot
Copy link
Copy Markdown
Member

sloriot commented Jan 5, 2016

CGAL_NO_ASSERTIONS is a macro keyword that is defined or not. That is you cannot use it as a boolean. You can add in Installation/include/CGAL/config.h:

    #ifndef CGAL_NO_ASSERTIONS
        #define CGAL_NO_ASSERTIONS_BOOL 0
    #else
       #define CGAL_NO_ASSERTIONS_BOOL 1
    #endif

and use it when using CGAL_NOEXCEPT.

@lrineau
Copy link
Copy Markdown
Member

lrineau commented Jan 5, 2016

That would be simpler to define CGAL_NO_ASSERTIONS to 1, when it is defined.

@sloriot
Copy link
Copy Markdown
Member

sloriot commented Jan 5, 2016

But then you're breaking the former usage of CGAL_NO_ASSERTIONS

@sloriot
Copy link
Copy Markdown
Member

sloriot commented Jan 5, 2016

and this will not solve the problem if it is not defined...

@lrineau
Copy link
Copy Markdown
Member

lrineau commented Jan 5, 2016

How would it be broken?! As long as the macro is never defined to 0, that makes no difference if CGAL_NO_ASSERTIONS is defined to 1 or to the empty string.

@GilesBathgate
Copy link
Copy Markdown
Contributor Author

@lrineau Because I use CGAL_NO_ASSERTIONS in place of false in commit GilesBathgate@0b11d8d
e.g. CGAL_NOEXCEPT(CGAL_NO_ASSERTIONS) as suggested by @mglisse

@GilesBathgate
Copy link
Copy Markdown
Contributor Author

@lrineau Furthermore I don't think an undefined macro evaluates to an empty string, it would give the compiler error: ‘CGAL_NO_ASSERTIONS’ was not declared in this scope

@lrineau
Copy link
Copy Markdown
Member

lrineau commented Jan 5, 2016

Currently in CGAL-4.7, the macro is either undefined, or defined to an empty string:

#define CGAL_NO_ASSERTIONS

You can use an undefined macro in #ifdef/#ifndef, but also in #if: in conditional preprocessing directives, any unknown identifier is replaced by 0 before the expression is evaluated. But that is the only place I know where you can use an undefined macro (undefined identifier). And that is not true for a macro that is defined to the empty string. With the following code:

#define A
#if A
#endif

the compiler returns an error:

test.cpp:2:6: error: #if with no expression
 #if A
      ^

That is why I would like that, in CGAL, we define macro that way:

  • either the macro is left undefined,
  • or it is defined to 1.

But, indeed, that would not be sufficient for you use CGAL_NOEXCEPT(CGAL_NO_ASSERTIONS).

@GilesBathgate
Copy link
Copy Markdown
Contributor Author

@lrineau Thanks that makes it clear. I think @sloriot solution is the cleanest in this situation.

This macro is always defined. Its value will be true when assertions are defined
and false when assertions are not defined. This macro can then be used in place
of a true false constant, such as is needed for CGAL_NOEXCEPT.
@sloriot
Copy link
Copy Markdown
Member

sloriot commented Jan 13, 2016

Merged in CGAL-4.8-Ic-99

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants