3

Suppose I have some C++ code which has a try-catch block in which the catch part will trigger a long jump:

#include <stdexcept>
#include <stdio.h>
#include <setjmp.h>

void my_fun()
{
    jmp_buf jump_buffer;
    if (setjmp(jump_buffer))
        return;
    
    try {
        std::string message;
        message.resize(100);
        snprintf(&message[0], 100, "error code %d\n", 3);
        throw std::runtime_error(message);
    }

    catch (std::runtime_error &e) {
        longjmp(jump_buffer, 1);
    }
}

Since the std::runtime_error object was allocated dynamically somewhere, will it leak the memory that was allocated for it or for the string?

anymous.asker
  • 1,179
  • 9
  • 14
  • 1
    That won't leak on a conforming implementation. Lifetime management of exception objects and proper unwinding when exception handling is combined with `longjmp` are definitely some of the more complicated/challenging things a C++ implementation has to deal with, so there's a likelihood of non-conformance in the real world. – Ben Voigt Sep 07 '21 at 17:00
  • 2
    To expand on what @BenVoigt said ["It depends"](https://godbolt.org/z/6hcx8fEn8), the runtime won't leak insofar as you return to the jump point it seems. – Mgetz Sep 07 '21 at 17:01

3 Answers3

7

This is kind of complicated. About longjmp's validity, the standard says:

A setjmp/longjmp call pair has undefined behavior if replacing the setjmp and longjmp by catch and throw would invoke any non-trivial destructors for any objects with automatic storage duration.

runtime_error has a non-trivial destructor, so the question is whether the exception object has "automatic storage duration". It does not. This suggests that longjmp should be fine.

In addition, exception object destruction can happen in one of two places:

The points of potential destruction for the exception object are:

  • when an active handler for the exception exits by any means other than rethrowing, immediately after the destruction of the object (if any) declared in the exception-declaration in the handler;

  • when an object of type std​::​exception_­ptr that refers to the exception object is destroyed, before the destructor of std​::​exception_­ptr returns.

longjmp is not "rethrowing". So in theory, this should be fine thanks to bullet point 1.

That being said, never rely on this. I highly doubt that implementations of longjmp handle this correctly, and even if some do, it's probably not something you can expect.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
4

There is actually a C++ defect report about this specific case, http://wg21.link/cwg2361 . So right now it is up in the air what is correct.

2

Why you would even want to do something like this is beyond me. But the question was intruiging. I know you can't jump into a catch block, and that jumping out of it (also goto :( ) is allowed.

It seems the exception is destructed just after leaving the catch scope. And nothing leaks. Here is the output of the test program below :

scope: normal_try_catch::try entered
exception constructed
scope : normal_try_catch::try left
scope : normal_try_catch::catch entered
scope : normal_try_catch::catch left
exception destructed
----------------------------
scope : long_jump_catch::try entered
exception constructed
scope : long_jump_catch::try left
scope : long_jump_catch::catch entered
scope : long_jump_catch::catch left
exception destructed
scope : long_jump_catch::leave because of setjmp entered
scope : long_jump_catch::leave because of setjmp left   

So the exception is cleaned up before the jump is made.

#include <stdexcept>
#include <iostream>
#include <string>
#include <setjmp.h>

class my_except :
    public std::exception
{
public:
    my_except() 
    {
        std::cout << "exception constructed" << std::endl;
    }

    ~my_except()
    {
        std::cout << "exception destructed" << std::endl;
    }
};

struct scope
{
    explicit scope(const std::string& scope) :
        m_scope{ scope }
    {
        std::cout << "scope : " << m_scope << " entered" << std::endl;
    }

    ~scope()
    {
        std::cout << "scope : " << m_scope << " left" << std::endl;
    }


private:
    std::string m_scope;
};


void normal_try_catch()
{
    try
    {
        scope s{ "normal_try_catch::try" };
        throw my_except();
    }
    catch(const std::exception&)
    {
        scope s{ "normal_try_catch::catch" };
    }
}

void long_jump_catch()
{
    jmp_buf jump_buffer;
    if (setjmp(jump_buffer))
    {
        scope s{ "long_jump_catch::leave because of setjmp" };
        return;
    }

    try
    {
        scope s{ "long_jump_catch::try" };
        throw my_except();
    }
    catch (const std::exception&)
    {
        scope s{ "long_jump_catch::catch" };
        longjmp(jump_buffer, 1);
    }
}


int main()
{
    normal_try_catch();
    std::cout << "----------------------------" << std::endl;
    long_jump_catch();
    return 0;
}
Pepijn Kramer
  • 9,356
  • 2
  • 8
  • 19
  • Thanks, this was very helpful. It seems however that if the jump is set instead to a point before the function call, the exception will actually end up leaking under GCC. And the reason I wanted to do this in the first place was to pass a callback to C code which does error handling through long jumps. – anymous.asker Sep 07 '21 at 17:42
  • After reading the standard, this behavior may be specific to MSVC (19). Since I am not invoking "trivial destructors", so if I understood correctly this is undefined behavior. – Pepijn Kramer Sep 08 '21 at 10:43