Skip to content

Commit d2ec836

Browse files
authored
Add support for nested C++11 exceptions (#3608)
* Add support for nested C++11 exceptions * Remove wrong include * Fix if directive * Fix missing skipif * Simplify code and try to work around MSVC bug * Clarify comment * Further simplify code * Remove the last extra throw statement * Qualify auto * Fix typo * Add missing return for consistency * Fix clang-tidy complaint * Fix python2 stub * Make clang-tidy happy * Fix compile error * Fix python2 function signature * Extract C++20 utility and backport * Cleanup code a bit more * Improve test case * Consolidate code and fix signature * Fix typo
1 parent f8d4aa4 commit d2ec836

File tree

4 files changed

+131
-13
lines changed

4 files changed

+131
-13
lines changed

include/pybind11/detail/common.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
# define PYBIND11_CPP14
3737
# if __cplusplus >= 201703L
3838
# define PYBIND11_CPP17
39+
# if __cplusplus >= 202002L
40+
# define PYBIND11_CPP20
41+
# endif
3942
# endif
4043
# endif
4144
#elif defined(_MSC_VER) && __cplusplus == 199711L
@@ -45,6 +48,9 @@
4548
# define PYBIND11_CPP14
4649
# if _MSVC_LANG > 201402L && _MSC_VER >= 1910
4750
# define PYBIND11_CPP17
51+
# if _MSVC_LANG >= 202002L
52+
# define PYBIND11_CPP20
53+
# endif
4854
# endif
4955
# endif
5056
#endif
@@ -612,6 +618,18 @@ template <typename T> using remove_cv_t = typename std::remove_cv<T>::type;
612618
template <typename T> using remove_reference_t = typename std::remove_reference<T>::type;
613619
#endif
614620

621+
#if defined(PYBIND11_CPP20)
622+
using std::remove_cvref;
623+
using std::remove_cvref_t;
624+
#else
625+
template <class T>
626+
struct remove_cvref {
627+
using type = remove_cv_t<remove_reference_t<T>>;
628+
};
629+
template <class T>
630+
using remove_cvref_t = typename remove_cvref<T>::type;
631+
#endif
632+
615633
/// Index sequences
616634
#if defined(PYBIND11_CPP14)
617635
using std::index_sequence;

include/pybind11/detail/internals.h

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#pragma once
1111

1212
#include "../pytypes.h"
13+
#include <exception>
1314

1415
/// Tracks the `internals` and `type_info` ABI version independent of the main library version.
1516
///
@@ -280,21 +281,104 @@ inline internals **&get_internals_pp() {
280281
return internals_pp;
281282
}
282283

284+
#if PY_VERSION_HEX >= 0x03030000
285+
// forward decl
286+
inline void translate_exception(std::exception_ptr);
287+
288+
template <class T,
289+
enable_if_t<std::is_same<std::nested_exception, remove_cvref_t<T>>::value, int> = 0>
290+
bool handle_nested_exception(const T &exc, const std::exception_ptr &p) {
291+
std::exception_ptr nested = exc.nested_ptr();
292+
if (nested != nullptr && nested != p) {
293+
translate_exception(nested);
294+
return true;
295+
}
296+
return false;
297+
}
298+
299+
template <class T,
300+
enable_if_t<!std::is_same<std::nested_exception, remove_cvref_t<T>>::value, int> = 0>
301+
bool handle_nested_exception(const T &exc, const std::exception_ptr &p) {
302+
if (auto *nep = dynamic_cast<const std::nested_exception *>(std::addressof(exc))) {
303+
return handle_nested_exception(*nep, p);
304+
}
305+
return false;
306+
}
307+
308+
#else
309+
310+
template <class T>
311+
bool handle_nested_exception(const T &, std::exception_ptr &) {
312+
return false;
313+
}
314+
#endif
315+
316+
inline bool raise_err(PyObject *exc_type, const char *msg) {
317+
#if PY_VERSION_HEX >= 0x03030000
318+
if (PyErr_Occurred()) {
319+
raise_from(exc_type, msg);
320+
return true;
321+
}
322+
#endif
323+
PyErr_SetString(exc_type, msg);
324+
return false;
325+
};
326+
283327
inline void translate_exception(std::exception_ptr p) {
328+
if (!p) {
329+
return;
330+
}
284331
try {
285-
if (p) std::rethrow_exception(p);
286-
} catch (error_already_set &e) { e.restore(); return;
287-
} catch (const builtin_exception &e) { e.set_error(); return;
288-
} catch (const std::bad_alloc &e) { PyErr_SetString(PyExc_MemoryError, e.what()); return;
289-
} catch (const std::domain_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
290-
} catch (const std::invalid_argument &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
291-
} catch (const std::length_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
292-
} catch (const std::out_of_range &e) { PyErr_SetString(PyExc_IndexError, e.what()); return;
293-
} catch (const std::range_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
294-
} catch (const std::overflow_error &e) { PyErr_SetString(PyExc_OverflowError, e.what()); return;
295-
} catch (const std::exception &e) { PyErr_SetString(PyExc_RuntimeError, e.what()); return;
332+
std::rethrow_exception(p);
333+
} catch (error_already_set &e) {
334+
handle_nested_exception(e, p);
335+
e.restore();
336+
return;
337+
} catch (const builtin_exception &e) {
338+
// Could not use template since it's an abstract class.
339+
if (auto *nep = dynamic_cast<const std::nested_exception *>(std::addressof(e))) {
340+
handle_nested_exception(*nep, p);
341+
}
342+
e.set_error();
343+
return;
344+
} catch (const std::bad_alloc &e) {
345+
handle_nested_exception(e, p);
346+
raise_err(PyExc_MemoryError, e.what());
347+
return;
348+
} catch (const std::domain_error &e) {
349+
handle_nested_exception(e, p);
350+
raise_err(PyExc_ValueError, e.what());
351+
return;
352+
} catch (const std::invalid_argument &e) {
353+
handle_nested_exception(e, p);
354+
raise_err(PyExc_ValueError, e.what());
355+
return;
356+
} catch (const std::length_error &e) {
357+
handle_nested_exception(e, p);
358+
raise_err(PyExc_ValueError, e.what());
359+
return;
360+
} catch (const std::out_of_range &e) {
361+
handle_nested_exception(e, p);
362+
raise_err(PyExc_IndexError, e.what());
363+
return;
364+
} catch (const std::range_error &e) {
365+
handle_nested_exception(e, p);
366+
raise_err(PyExc_ValueError, e.what());
367+
return;
368+
} catch (const std::overflow_error &e) {
369+
handle_nested_exception(e, p);
370+
raise_err(PyExc_OverflowError, e.what());
371+
return;
372+
} catch (const std::exception &e) {
373+
handle_nested_exception(e, p);
374+
raise_err(PyExc_RuntimeError, e.what());
375+
return;
376+
} catch (const std::nested_exception &e) {
377+
handle_nested_exception(e, p);
378+
raise_err(PyExc_RuntimeError, "Caught an unknown nested exception!");
379+
return;
296380
} catch (...) {
297-
PyErr_SetString(PyExc_RuntimeError, "Caught an unknown exception!");
381+
raise_err(PyExc_RuntimeError, "Caught an unknown exception!");
298382
return;
299383
}
300384
}

tests/test_exceptions.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#include "local_bindings.h"
1212

1313
#include "pybind11_tests.h"
14+
#include <exception>
15+
#include <stdexcept>
1416
#include <utility>
1517

1618
// A type that should be raised as an exception in Python
@@ -105,7 +107,6 @@ struct PythonAlreadySetInDestructor {
105107
py::str s;
106108
};
107109

108-
109110
TEST_SUBMODULE(exceptions, m) {
110111
m.def("throw_std_exception", []() {
111112
throw std::runtime_error("This exception was intentionally thrown.");
@@ -281,5 +282,12 @@ TEST_SUBMODULE(exceptions, m) {
281282
}
282283
});
283284

285+
m.def("throw_nested_exception", []() {
286+
try {
287+
throw std::runtime_error("Inner Exception");
288+
} catch (const std::runtime_error &) {
289+
std::throw_with_nested(std::runtime_error("Outer Exception"));
290+
}
291+
});
284292
#endif
285293
}

tests/test_exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,14 @@ def pycatch(exctype, f, *args):
239239
assert str(excinfo.value) == "this is a helper-defined translated exception"
240240

241241

242+
@pytest.mark.skipif("env.PY2")
243+
def test_throw_nested_exception():
244+
with pytest.raises(RuntimeError) as excinfo:
245+
m.throw_nested_exception()
246+
assert str(excinfo.value) == "Outer Exception"
247+
assert str(excinfo.value.__cause__) == "Inner Exception"
248+
249+
242250
# This can often happen if you wrap a pybind11 class in a Python wrapper
243251
def test_invalid_repr():
244252
class MyRepr(object):

0 commit comments

Comments
 (0)