mirror of
https://github.com/pybind/pybind11.git
synced 2024-11-23 21:55:11 +00:00
2e260b067f
* `container: silkeh/clang:18-bookworm` in .github/workflows/format.yml * clang-tidy auto-fix (trivial, in test only) * Disable `performance-enum-size` (noisy, low value) * Temporarily turn off 3 diagnostics (to be tackled one-by-one). * Add explicit `switch` `default` to resolve clang-tidy `bugprone-switch-missing-default-case` Debian clang version 18.1.8 (++20240718080534+3b5b5c1ec4a3-1~exp1~20240718200641.143) Target: x86_64-pc-linux-gnu tests/test_numpy_dtypes.cpp:212:5: warning: switching on non-enum value without default case may not cover all cases [bugprone-switch-missing-default-case] * Add clang-17 and clang-18 testing. * Add `NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange)` in test_tagbased_polymorphic.cpp Debian clang version 18.1.8 (++20240718080534+3b5b5c1ec4a3-1~exp1~20240718200641.143) Target: x86_64-pc-linux-gnu tests/test_tagbased_polymorphic.cpp:77:40: warning: The value '150' provided to the cast expression is not in the valid range of values for 'Kind' [clang-analyzer-optin.core.EnumCastOutOfRange] * Fix inconsistent pybind11/eigen/tensor.h behavior: This existing comment in pybind11/eigen/tensor.h ``` // move, take_ownership don't make any sense for a ref/map: ``` is at odds with the `delete src;` three lines up. In real-world client code `take_ownership` will not exist (unless the client code is untested and unused). I.e. the `delete` is essentially only useful to avoid leaks in the pybind11 unit tests. While upgrading to clang-tidy 18, the warning below appeared. Apparently it is produced during LTO, and it appears difficult to suppress. Regardless, the best way to resolve this is to remove the `delete` and to simply make the test objects `static` in the unit test code. ________ Debian clang version 18.1.8 (++20240718080534+3b5b5c1ec4a3-1~exp1~20240718200641.143) Target: x86_64-pc-linux-gnu ________ ``` lto-wrapper: warning: using serial compilation of 3 LTRANS jobs lto-wrapper: note: see the ‘-flto’ option documentation for more information In function ‘cast_impl’, inlined from ‘cast’ at /mounted_pybind11/include/pybind11/eigen/tensor.h:414:25, inlined from ‘operator()’ at /mounted_pybind11/include/pybind11/eigen/../pybind11.h:296:40, inlined from ‘_FUN’ at /mounted_pybind11/include/pybind11/eigen/../pybind11.h:267:21: /mounted_pybind11/include/pybind11/eigen/tensor.h:475:17: warning: ‘operator delete’ called on unallocated object ‘<anonymous>’ [-Wfree-nonheap-object] 475 | delete src; | ^ /mounted_pybind11/include/pybind11/eigen/../pybind11.h: In function ‘_FUN’: /mounted_pybind11/include/pybind11/eigen/../pybind11.h:297:75: note: declared here 297 | std::move(args_converter).template call<Return, Guard>(cap->f), | ^ ``` * Disable `bugprone-chained-comparison`: this clang-tidy check is incompatible with the Catch2 `REQUIRE` macro (26 warnings like the one below). ________ Debian clang version 18.1.8 (++20240718080534+3b5b5c1ec4a3-1~exp1~20240718200641.143) Target: x86_64-pc-linux-gnu ________ ``` /mounted_pybind11/tests/test_embed/test_interpreter.cpp:127:9: warning: chained comparison 'v0 <= v1 == v2' may generate unintended results, use parentheses to specify order of evaluation or a logical operator to separate comparison expressions [bugprone-chained-comparison] 127 | REQUIRE(ret == 42); | ^ /build/tests/catch/catch.hpp:17670:24: note: expanded from macro 'REQUIRE' 17670 | #define REQUIRE( ... ) INTERNAL_CATCH_TEST( "REQUIRE", Catch::ResultDisposition::Normal, __VA_ARGS__ ) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /build/tests/catch/catch.hpp:2710:47: note: expanded from macro 'INTERNAL_CATCH_TEST' 2710 | catchAssertionHandler.handleExpr( Catch::Decomposer() <= __VA_ARGS__ ); \ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /mounted_pybind11/tests/test_embed/test_interpreter.cpp:127:9: note: operand 'v0' is here 127 | REQUIRE(ret == 42); | ^ /build/tests/catch/catch.hpp:17670:24: note: expanded from macro 'REQUIRE' 17670 | #define REQUIRE( ... ) INTERNAL_CATCH_TEST( "REQUIRE", Catch::ResultDisposition::Normal, __VA_ARGS__ ) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /build/tests/catch/catch.hpp:2710:47: note: expanded from macro 'INTERNAL_CATCH_TEST' 2710 | catchAssertionHandler.handleExpr( Catch::Decomposer() <= __VA_ARGS__ ); \ | ^~~~~~~~~~~~~~~~~~~ /mounted_pybind11/tests/test_embed/test_interpreter.cpp:127:17: note: operand 'v1' is here 127 | REQUIRE(ret == 42); | ^ /build/tests/catch/catch.hpp:17670:90: note: expanded from macro 'REQUIRE' 17670 | #define REQUIRE( ... ) INTERNAL_CATCH_TEST( "REQUIRE", Catch::ResultDisposition::Normal, __VA_ARGS__ ) | ^~~~~~~~~~~ /build/tests/catch/catch.hpp:2710:70: note: expanded from macro 'INTERNAL_CATCH_TEST' 2710 | catchAssertionHandler.handleExpr( Catch::Decomposer() <= __VA_ARGS__ ); \ | ^~~~~~~~~~~ /mounted_pybind11/tests/test_embed/test_interpreter.cpp:127:24: note: operand 'v2' is here 127 | REQUIRE(ret == 42); | ^ /build/tests/catch/catch.hpp:17670:90: note: expanded from macro 'REQUIRE' 17670 | #define REQUIRE( ... ) INTERNAL_CATCH_TEST( "REQUIRE", Catch::ResultDisposition::Normal, __VA_ARGS__ ) | ^~~~~~~~~~~ /build/tests/catch/catch.hpp:2710:70: note: expanded from macro 'INTERNAL_CATCH_TEST' 2710 | catchAssertionHandler.handleExpr( Catch::Decomposer() <= __VA_ARGS__ ); \ | ^~~~~~~~~~~ ``` * Add 8 `// NOLINT(bugprone-empty-catch)` * Resolve clang-tidy `bugprone-multi-level-implicit-pointer-conversion` warnings. ________ Debian clang version 18.1.8 (++20240718080534+3b5b5c1ec4a3-1~exp1~20240718200641.143) Target: x86_64-pc-linux-gnu ________ ``` pybind11/detail/internals.h:556:53: warning: multilevel pointer conversion from 'internals **' to 'const void *', please use explicit cast [bugprone-multi-level-implicit-pointer-conversion] pybind11/detail/type_caster_base.h:431:20: warning: multilevel pointer conversion from 'void **' to 'void *', please use explicit cast [bugprone-multi-level-implicit-pointer-conversion] pybind11/numpy.h:904:81: warning: multilevel pointer conversion from '_object *const *' to 'const void *', please use explicit cast [bugprone-multi-level-implicit-pointer-conversion] pybind11/numpy.h:1989:39: warning: multilevel pointer conversion from 'typename vectorize_arg<const double *>::type *' (aka 'const double **') to 'void *', please use explicit cast [bugprone-multi-level-implicit-pointer-conversion] pybind11/numpy.h:1989:39: warning: multilevel pointer conversion from 'typename vectorize_arg<const VectorizeTestClass *>::type *' (aka 'const VectorizeTestClass **') to 'void *', please use explicit cast [bugprone-multi-level-implicit-pointer-conversion] pybind11/stl/filesystem.h:75:44: warning: multilevel pointer conversion from 'PyObject **' (aka '_object **') to 'void *', please use explicit cast [bugprone-multi-level-implicit-pointer-conversion] pybind11/stl/filesystem.h:83:42: warning: multilevel pointer conversion from 'PyObject **' (aka '_object **') to 'void *', please use explicit cast [bugprone-multi-level-implicit-pointer-conversion] ``` * Introduce `PYBIND11_REINTERPRET_CAST_VOID_PTR_IF_NOT_PYPY` to resolve PyPy build errors: ``` In file included from /Users/runner/work/pybind11/pybind11/tests/test_stl.cpp:18: /Users/runner/work/pybind11/pybind11/include/pybind11/stl/filesystem.h:75:17: error: no matching function for call to 'PyPyUnicode_FSConverter' if (PyUnicode_FSConverter(buf, reinterpret_cast<void *>(&native)) != 0) { ^~~~~~~~~~~~~~~~~~~~~ /Users/runner/hostedtoolcache/PyPy/3.10.14/x64/include/pypy3.10/pypy_decl.h:969:31: note: expanded from macro 'PyUnicode_FSConverter' ^~~~~~~~~~~~~~~~~~~~~~~ /Users/runner/hostedtoolcache/PyPy/3.10.14/x64/include/pypy3.10/pypy_decl.h:970:17: note: candidate function not viable: cannot convert argument of incomplete type 'void *' to 'struct _object **' for 2nd argument PyAPI_FUNC(int) PyUnicode_FSConverter(struct _object *arg0, struct _object **arg1); ^ /Users/runner/hostedtoolcache/PyPy/3.10.14/x64/include/pypy3.10/pypy_decl.h:969:31: note: expanded from macro 'PyUnicode_FSConverter' ^ In file included from /Users/runner/work/pybind11/pybind11/tests/test_stl.cpp:18: /Users/runner/work/pybind11/pybind11/include/pybind11/stl/filesystem.h:83:17: error: no matching function for call to 'PyPyUnicode_FSDecoder' if (PyUnicode_FSDecoder(buf, reinterpret_cast<void *>(&native)) != 0) { ^~~~~~~~~~~~~~~~~~~ /Users/runner/hostedtoolcache/PyPy/3.10.14/x64/include/pypy3.10/pypy_decl.h:971:29: note: expanded from macro 'PyUnicode_FSDecoder' ^~~~~~~~~~~~~~~~~~~~~ /Users/runner/hostedtoolcache/PyPy/3.10.14/x64/include/pypy3.10/pypy_decl.h:972:17: note: candidate function not viable: cannot convert argument of incomplete type 'void *' to 'struct _object **' for 2nd argument PyAPI_FUNC(int) PyUnicode_FSDecoder(struct _object *arg0, struct _object **arg1); ^ /Users/runner/hostedtoolcache/PyPy/3.10.14/x64/include/pypy3.10/pypy_decl.h:971:29: note: expanded from macro 'PyUnicode_FSDecoder' ^ ``` * clang-tidy auto-fix * Fix silly oversight.
323 lines
12 KiB
C++
323 lines
12 KiB
C++
#pragma once
|
|
/*
|
|
tests/constructor_stats.h -- framework for printing and tracking object
|
|
instance lifetimes in example/test code.
|
|
|
|
Copyright (c) 2016 Jason Rhinelander <jason@imaginary.ca>
|
|
|
|
All rights reserved. Use of this source code is governed by a
|
|
BSD-style license that can be found in the LICENSE file.
|
|
|
|
This header provides a few useful tools for writing examples or tests that want to check and/or
|
|
display object instance lifetimes. It requires that you include this header and add the following
|
|
function calls to constructors:
|
|
|
|
class MyClass {
|
|
MyClass() { ...; print_default_created(this); }
|
|
~MyClass() { ...; print_destroyed(this); }
|
|
MyClass(const MyClass &c) { ...; print_copy_created(this); }
|
|
MyClass(MyClass &&c) { ...; print_move_created(this); }
|
|
MyClass(int a, int b) { ...; print_created(this, a, b); }
|
|
MyClass &operator=(const MyClass &c) { ...; print_copy_assigned(this); }
|
|
MyClass &operator=(MyClass &&c) { ...; print_move_assigned(this); }
|
|
|
|
...
|
|
}
|
|
|
|
You can find various examples of these in several of the existing testing .cpp files. (Of course
|
|
you don't need to add any of the above constructors/operators that you don't actually have, except
|
|
for the destructor).
|
|
|
|
Each of these will print an appropriate message such as:
|
|
|
|
### MyClass @ 0x2801910 created via default constructor
|
|
### MyClass @ 0x27fa780 created 100 200
|
|
### MyClass @ 0x2801910 destroyed
|
|
### MyClass @ 0x27fa780 destroyed
|
|
|
|
You can also include extra arguments (such as the 100, 200 in the output above, coming from the
|
|
value constructor) for all of the above methods which will be included in the output.
|
|
|
|
For testing, each of these also keeps track the created instances and allows you to check how many
|
|
of the various constructors have been invoked from the Python side via code such as:
|
|
|
|
from pybind11_tests import ConstructorStats
|
|
cstats = ConstructorStats.get(MyClass)
|
|
print(cstats.alive())
|
|
print(cstats.default_constructions)
|
|
|
|
Note that `.alive()` should usually be the first thing you call as it invokes Python's garbage
|
|
collector to actually destroy objects that aren't yet referenced.
|
|
|
|
For everything except copy and move constructors and destructors, any extra values given to the
|
|
print_...() function is stored in a class-specific values list which you can retrieve and inspect
|
|
from the ConstructorStats instance `.values()` method.
|
|
|
|
In some cases, when you need to track instances of a C++ class not registered with pybind11, you
|
|
need to add a function returning the ConstructorStats for the C++ class; this can be done with:
|
|
|
|
m.def("get_special_cstats", &ConstructorStats::get<SpecialClass>,
|
|
py::return_value_policy::reference)
|
|
|
|
Finally, you can suppress the output messages, but keep the constructor tracking (for
|
|
inspection/testing in python) by using the functions with `print_` replaced with `track_` (e.g.
|
|
`track_copy_created(this)`).
|
|
|
|
*/
|
|
|
|
#include "pybind11_tests.h"
|
|
|
|
#include <list>
|
|
#include <sstream>
|
|
#include <typeindex>
|
|
#include <unordered_map>
|
|
|
|
class ConstructorStats {
|
|
protected:
|
|
std::unordered_map<void *, int> _instances; // Need a map rather than set because members can
|
|
// shared address with parents
|
|
std::list<std::string> _values; // Used to track values
|
|
// (e.g. of value constructors)
|
|
public:
|
|
int default_constructions = 0;
|
|
int copy_constructions = 0;
|
|
int move_constructions = 0;
|
|
int copy_assignments = 0;
|
|
int move_assignments = 0;
|
|
|
|
void copy_created(void *inst) {
|
|
created(inst);
|
|
copy_constructions++;
|
|
}
|
|
|
|
void move_created(void *inst) {
|
|
created(inst);
|
|
move_constructions++;
|
|
}
|
|
|
|
void default_created(void *inst) {
|
|
created(inst);
|
|
default_constructions++;
|
|
}
|
|
|
|
void created(void *inst) { ++_instances[inst]; }
|
|
|
|
void destroyed(void *inst) {
|
|
if (--_instances[inst] < 0) {
|
|
throw std::runtime_error("cstats.destroyed() called with unknown "
|
|
"instance; potential double-destruction "
|
|
"or a missing cstats.created()");
|
|
}
|
|
}
|
|
|
|
static void gc() {
|
|
// Force garbage collection to ensure any pending destructors are invoked:
|
|
#if defined(PYPY_VERSION)
|
|
PyObject *globals = PyEval_GetGlobals();
|
|
PyObject *result = PyRun_String("import gc\n"
|
|
"for i in range(2):\n"
|
|
" gc.collect()\n",
|
|
Py_file_input,
|
|
globals,
|
|
globals);
|
|
if (result == nullptr)
|
|
throw py::error_already_set();
|
|
Py_DECREF(result);
|
|
#else
|
|
py::module_::import("gc").attr("collect")();
|
|
#endif
|
|
}
|
|
|
|
int alive() {
|
|
gc();
|
|
int total = 0;
|
|
for (const auto &p : _instances) {
|
|
if (p.second > 0) {
|
|
total += p.second;
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
|
|
void value() {} // Recursion terminator
|
|
// Takes one or more values, converts them to strings, then stores them.
|
|
template <typename T, typename... Tmore>
|
|
void value(const T &v, Tmore &&...args) {
|
|
std::ostringstream oss;
|
|
oss << v;
|
|
_values.push_back(oss.str());
|
|
value(std::forward<Tmore>(args)...);
|
|
}
|
|
|
|
// Move out stored values
|
|
py::list values() {
|
|
py::list l;
|
|
for (const auto &v : _values) {
|
|
l.append(py::cast(v));
|
|
}
|
|
_values.clear();
|
|
return l;
|
|
}
|
|
|
|
// Gets constructor stats from a C++ type index
|
|
static ConstructorStats &get(std::type_index type) {
|
|
static std::unordered_map<std::type_index, ConstructorStats> all_cstats;
|
|
return all_cstats[type];
|
|
}
|
|
|
|
// Gets constructor stats from a C++ type
|
|
template <typename T>
|
|
static ConstructorStats &get() {
|
|
#if defined(PYPY_VERSION)
|
|
gc();
|
|
#endif
|
|
return get(typeid(T));
|
|
}
|
|
|
|
// Gets constructor stats from a Python class
|
|
static ConstructorStats &get(py::object class_) {
|
|
auto &internals = py::detail::get_internals();
|
|
const std::type_index *t1 = nullptr, *t2 = nullptr;
|
|
try {
|
|
auto *type_info
|
|
= internals.registered_types_py.at((PyTypeObject *) class_.ptr()).at(0);
|
|
for (auto &p : internals.registered_types_cpp) {
|
|
if (p.second == type_info) {
|
|
if (t1) {
|
|
t2 = &p.first;
|
|
break;
|
|
}
|
|
t1 = &p.first;
|
|
}
|
|
}
|
|
} catch (const std::out_of_range &) { // NOLINT(bugprone-empty-catch)
|
|
}
|
|
if (!t1) {
|
|
throw std::runtime_error("Unknown class passed to ConstructorStats::get()");
|
|
}
|
|
auto &cs1 = get(*t1);
|
|
// If we have both a t1 and t2 match, one is probably the trampoline class; return
|
|
// whichever has more constructions (typically one or the other will be 0)
|
|
if (t2) {
|
|
auto &cs2 = get(*t2);
|
|
int cs1_total = cs1.default_constructions + cs1.copy_constructions
|
|
+ cs1.move_constructions + (int) cs1._values.size();
|
|
int cs2_total = cs2.default_constructions + cs2.copy_constructions
|
|
+ cs2.move_constructions + (int) cs2._values.size();
|
|
if (cs2_total > cs1_total) {
|
|
return cs2;
|
|
}
|
|
}
|
|
return cs1;
|
|
}
|
|
};
|
|
|
|
// To track construction/destruction, you need to call these methods from the various
|
|
// constructors/operators. The ones that take extra values record the given values in the
|
|
// constructor stats values for later inspection.
|
|
template <class T>
|
|
void track_copy_created(T *inst) {
|
|
ConstructorStats::get<T>().copy_created(inst);
|
|
}
|
|
template <class T>
|
|
void track_move_created(T *inst) {
|
|
ConstructorStats::get<T>().move_created(inst);
|
|
}
|
|
template <class T, typename... Values>
|
|
void track_copy_assigned(T *, Values &&...values) {
|
|
auto &cst = ConstructorStats::get<T>();
|
|
cst.copy_assignments++;
|
|
cst.value(std::forward<Values>(values)...);
|
|
}
|
|
template <class T, typename... Values>
|
|
void track_move_assigned(T *, Values &&...values) {
|
|
auto &cst = ConstructorStats::get<T>();
|
|
cst.move_assignments++;
|
|
cst.value(std::forward<Values>(values)...);
|
|
}
|
|
template <class T, typename... Values>
|
|
void track_default_created(T *inst, Values &&...values) {
|
|
auto &cst = ConstructorStats::get<T>();
|
|
cst.default_created(inst);
|
|
cst.value(std::forward<Values>(values)...);
|
|
}
|
|
template <class T, typename... Values>
|
|
void track_created(T *inst, Values &&...values) {
|
|
auto &cst = ConstructorStats::get<T>();
|
|
cst.created(inst);
|
|
cst.value(std::forward<Values>(values)...);
|
|
}
|
|
template <class T, typename... Values>
|
|
void track_destroyed(T *inst) {
|
|
ConstructorStats::get<T>().destroyed(inst);
|
|
}
|
|
template <class T, typename... Values>
|
|
void track_values(T *, Values &&...values) {
|
|
ConstructorStats::get<T>().value(std::forward<Values>(values)...);
|
|
}
|
|
|
|
/// Don't cast pointers to Python, print them as strings
|
|
inline const char *format_ptrs(const char *p) { return p; }
|
|
template <typename T>
|
|
py::str format_ptrs(T *p) {
|
|
return "{:#x}"_s.format(reinterpret_cast<std::uintptr_t>(p));
|
|
}
|
|
template <typename T>
|
|
auto format_ptrs(T &&x) -> decltype(std::forward<T>(x)) {
|
|
return std::forward<T>(x);
|
|
}
|
|
|
|
template <class T, typename... Output>
|
|
void print_constr_details(T *inst, const std::string &action, Output &&...output) {
|
|
py::print("###",
|
|
py::type_id<T>(),
|
|
"@",
|
|
format_ptrs(inst),
|
|
action,
|
|
format_ptrs(std::forward<Output>(output))...);
|
|
}
|
|
|
|
// Verbose versions of the above:
|
|
template <class T, typename... Values>
|
|
void print_copy_created(T *inst,
|
|
Values &&...values) { // NB: this prints, but doesn't store, given values
|
|
print_constr_details(inst, "created via copy constructor", values...);
|
|
track_copy_created(inst);
|
|
}
|
|
template <class T, typename... Values>
|
|
void print_move_created(T *inst,
|
|
Values &&...values) { // NB: this prints, but doesn't store, given values
|
|
print_constr_details(inst, "created via move constructor", values...);
|
|
track_move_created(inst);
|
|
}
|
|
template <class T, typename... Values>
|
|
void print_copy_assigned(T *inst, Values &&...values) {
|
|
print_constr_details(inst, "assigned via copy assignment", values...);
|
|
track_copy_assigned(inst, values...);
|
|
}
|
|
template <class T, typename... Values>
|
|
void print_move_assigned(T *inst, Values &&...values) {
|
|
print_constr_details(inst, "assigned via move assignment", values...);
|
|
track_move_assigned(inst, values...);
|
|
}
|
|
template <class T, typename... Values>
|
|
void print_default_created(T *inst, Values &&...values) {
|
|
print_constr_details(inst, "created via default constructor", values...);
|
|
track_default_created(inst, values...);
|
|
}
|
|
template <class T, typename... Values>
|
|
void print_created(T *inst, Values &&...values) {
|
|
print_constr_details(inst, "created", values...);
|
|
track_created(inst, values...);
|
|
}
|
|
template <class T, typename... Values>
|
|
void print_destroyed(T *inst, Values &&...values) { // Prints but doesn't store given values
|
|
print_constr_details(inst, "destroyed", values...);
|
|
track_destroyed(inst);
|
|
}
|
|
template <class T, typename... Values>
|
|
void print_values(T *inst, Values &&...values) {
|
|
print_constr_details(inst, ":", values...);
|
|
track_values(inst, values...);
|
|
}
|