Throw TypeError when subclasses forget to call __init__ - #2152
Conversation
0146834 to
5e5d3cf
Compare
|
This change looks great, and I am happy to merge it. Performance impact should be minimal as you say. But can you please fix pep8? (See the failing "STYLE" test). |
|
In particular, there is a long line with the error message string that should be split. |
5e5d3cf to
8383222
Compare
|
FIxed, sorry for the delay! |
|
Ready for merge @wjakob , thanks! |
8383222 to
674a400
Compare
|
Still ready. :) |
|
Ping. |
|
Ping? :( |
YannickJadoul
left a comment
There was a problem hiding this comment.
As gently pinged by @virtuald on Gitter, this should be ready to finish, as it was already reviewed.
So this functionality is not present when a custom metaclass is used? For completeness, it might be nice to mention that in the docs, still.
|
Thanks @virtuald, and for the reminder @YannickJadoul. |
|
@YannickJadoul the docs I added indirectly say what you want?
Implying that if you set your own metaclass then you'll get different behavior... ? |
OK, yes, that's a bit implicit, but good enough. Using custom metaclasses comes with a lot of caveats anyway, currently. |
Cherry-pick of pybind#2152 Co-authored-by: Dustin Spicuzza <dustin@virtualroadside.com>
|
It turns out this PR #2152 leads to a test breakage related to this OSS deepmind code: The situation top-down:
When |
Are you sure? "No |
|
When naively adding a call to Looking at the wrapper code (pyspiel.cc link in previous comment), there isn't an But I'm not sure where to start fixing. Fake an |
|
This is just checking to see if this has been initialized; currently it is not. If pyspiel.Game is not default constructible, why would SomeGame be? It needs to call one of the factory functions, or a constructor of some sort, or otherwise pyspiel.Game is not fully constructed. |
Thanks Henry, I'll dig in deeper to see what they are doing currently. I know eventually they are calling |
|
It's possible that inheriting from a class implemented only by a factory function might not be properly constructing the holder? Seems weird. |
|
I've now just spent half an hour looking around into this code and pybind11's dark magic, thinking whether there ought to be a way to surpass this check when a class is used as "interface", but it feels rather dangerous since it allows to access uninitialized data from Python (triggering UB from Python). There's a way to override The docs should be updated indeed; that part must've been overlooked when merging this PR. I'll go over it and see if I can fix it.
I'm not sure this is something we'd want in the error message, though. This is a message that users of a pybind11-library might get to see, and pybind11's docs are meant as a reference for the developers. You don't want to confuse Python users with C++ thing, if you ask me. I was not a fan of the error message either (since it's basically just 2 steps: a. you need to call
Careful with this, though. Inheritance in pybind11 is complex and can be subtle, so I'd prefer users would read the whole section. And if someone already knows how pybind11 interacts with inheritance and subclassing, it should be easy enough to scan that section and find the relevant part, no? |
|
@rwgk Actually, in the case of class X {
public:
static X create() { return X(42); }
int getZ() const { return z; }
virtual ~X() = default;
private:
X(int y) : z(y) {}
int z;
};
void f(const X &x) {
py::print(x.getZ());
}
class PyX : public X {
public:
PyX() : X(X::create()) {}
};
PYBIND11_MODULE(example, m)
{
py::class_<X, PyX>(m, "X")
.def_property_readonly("z", &X::getZ)
.def(py::init([]() { throw py::type_error("Cannot construct an X object without subclassing."); return static_cast<X*>(nullptr); },
[]() { return PyX(); }));
m.def("create", &X::create);
m.def("f", &f);
}This results in: Python 3.8.0 (default, Oct 28 2019, 16:14:01)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import example
>>> example.X
<class 'example.X'>
>>> example.X()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Cannot construct an X object without subclassing.
>>> class Y:
...
KeyboardInterrupt
>>> class Y(example.X):
... pass
...
>>> Y()
<__main__.Y object at 0x7f1bdfc8a360>
>>> class Y(example.X):
... def __init__(self):
... example.X.__init__(self)
...
>>> Y()
<__main__.Y object at 0x7f1bdfc93f40>
>>> |
|
See #2429. |
Hi @YannickJadoul, my current solution is this: There is a list of known/allowed names to pick from. It works with one I picked randomly. I asked for advice about creating a "bare" game, to use your word for it. Let's see what they say. To have an Mostly out of curiosity, I also tried this: It builds fine but |
Nice, you bumped into a bug! :-D I managed to figure out what goes wrong; see #2430.
Right, I understand. I was mostly trying to point out the trampoline class, which allows to add the |
|
Ha, I found a hacky way to determine if the constructor was overridden or not. I'll make a PR. |
Hidden bug discovered while preparing for a third_party/pybind11 update (testing with current pybind11 github master branch). Backward compatible. Relevant pybind11 change: pybind/pybind11#2152 PiperOrigin-RevId: 327643330 Change-Id: Ic867f6d1e1932c3898d4a6f8fb63acde4739204b
Backward compatible change preparing for pybind11 update. Relevant pybind11 change: * pybind/pybind11#2152 * Throw TypeError when subclasses forget to call __init__ PiperOrigin-RevId: 328182655 Change-Id: I8dc8cd69ee328f95bdca58b0f9045d65d11307a8
… *>()` introduced with PR pybind#2152 The `reinterpret_cast<instance *>(self)` is unsafe if `__new__` is mocked, which was actually found in the wild: the mock returned `None` for `self`. This was inconsequential because `inst` is currently cast straight back to `PyObject *` to compute `all_type_info()`, which is empty if `self` is not a pybind11 `instance`, and then `inst` is never dereferenced. However, the unsafe detour through `instance *` is easily avoided and the updated implementation is less prone to accidents while debugging or refactoring.
#4762) * Equivalent of google/clif@5718e4d * Resolve clang-tidy errors. * Moving test_PPCCInit() first changes the behavior! * Resolve new Clang dev C++11 errors: ``` The CXX compiler identification is Clang 17.0.0 ``` ``` pytypes.h:1615:23: error: identifier '_s' preceded by whitespace in a literal operator declaration is deprecated [-Werror,-Wdeprecated-literal-operator] ``` ``` cast.h:1380:26: error: identifier '_a' preceded by whitespace in a literal operator declaration is deprecated [-Werror,-Wdeprecated-literal-operator] ``` * Resolve gcc 4.8.5 error: ``` pytypes.h:1615:12: error: missing space between '""' and suffix identifier ``` * Specifically exclude `__clang__` * Snapshot of debugging code (does NOT pass pre-commit checks). * Revert "Snapshot of debugging code (does NOT pass pre-commit checks)." This reverts commit 1d4f9ff. * [ci skip] Order Dependence Demo * Revert "[ci skip] Order Dependence Demo" This reverts commit d37b540. * One way to deal with the order dependency issue. This is not the best way, more like a proof of concept. * Move test_PC() first again. * Add `all_type_info_add_base_most_derived_first()`, use in `all_type_info_populate()` * Revert "One way to deal with the order dependency issue. This is not the best way, more like a proof of concept." This reverts commit eb09c6c. * clang-tidy fixes (automatic) * Add `is_redundant_value_and_holder()` and use to avoid forcing `__init__` overrides when they are not needed. * Streamline implementation and avoid unsafe `reinterpret_cast<instance *>()` introduced with PR #2152 The `reinterpret_cast<instance *>(self)` is unsafe if `__new__` is mocked, which was actually found in the wild: the mock returned `None` for `self`. This was inconsequential because `inst` is currently cast straight back to `PyObject *` to compute `all_type_info()`, which is empty if `self` is not a pybind11 `instance`, and then `inst` is never dereferenced. However, the unsafe detour through `instance *` is easily avoided and the updated implementation is less prone to accidents while debugging or refactoring. * Fix actual undefined behavior exposed by previous changes. It turns out the previous commit message is incorrect, the `inst` pointer is actually dereferenced, in the `value_and_holder` ctor here: https://github.com/pybind/pybind11/blob/f3e0602802c7840992c97f4960515777cad6a5c7/include/pybind11/detail/type_caster_base.h#L262-L263 ``` 259 // Main constructor for a found value/holder: 260 value_and_holder(instance *i, const detail::type_info *type, size_t vpos, size_t index) 261 : inst{i}, index{index}, type{type}, 262 vh{inst->simple_layout ? inst->simple_value_holder 263 : &inst->nonsimple.values_and_holders[vpos]} {} ``` * Add test_mock_new() * Experiment: specify indirect bases * Revert "Experiment: specify indirect bases" This reverts commit 4f90d85. * Add `all_type_info_check_for_divergence()` and some tests. * Call `all_type_info_check_for_divergence()` also from `type_caster_generic::load_impl<>` * Resolve clang-tidy error: ``` include/pybind11/detail/type_caster_base.h:795:21: error: the 'empty' method should be used to check for emptiness instead of 'size' [readability-container-size-empty,-warnings-as-errors] if (matching_bases.size() != 0) { ^~~~~~~~~~~~~~~~~~~~~~~~~~ !matching_bases.empty() ``` * Revert "Resolve clang-tidy error:" This reverts commit df27188. * Revert "Call `all_type_info_check_for_divergence()` also from `type_caster_generic::load_impl<>`" This reverts commit 5f5fd6a. * Revert "Add `all_type_info_check_for_divergence()` and some tests." This reverts commit 0a9599f.
* Call from new `tp_init_intercepted()` (adopting mechanism first added in PyCLIF: google/clif@7cba87d). * Remove `pybind11_meta_call()` (which was added with pybind/pybind11#2152).
* Call from new `tp_init_intercepted()` (adopting mechanism first added in PyCLIF: google/clif@7cba87d). * Remove `pybind11_meta_call()` (which was added with pybind/pybind11#2152).
…ng __init__` safety feature to work for any metaclass. (#30095) * Also wrap with `py::metaclass((PyObject *) &PyType_Type)` * Transfer additional tests from PyCLIF python_multiple_inheritance_test.py * Expand tests to fully cover wrapping with alternative metaclasses. * * Factor out `ensure_base_init_functions_were_called()`. * Call from new `tp_init_intercepted()` (adopting mechanism first added in PyCLIF: google/clif@7cba87d). * Remove `pybind11_meta_call()` (which was added with pybind/pybind11#2152). * Bug fix (maybe actually two bugs?): simplify condition to `type->tp_init != tp_init_intercepted` * Removing `Py_DECREF(self)` that leads to MSAN failure (Google toolchain). ``` ==6380==WARNING: MemorySanitizer: use-of-uninitialized-value #0 0x5611589c9a58 in Py_DECREF third_party/python_runtime/v3_11/Include/object.h:537:9 ... Uninitialized value was created by a heap deallocation #0 0x5611552757b0 in free third_party/llvm/llvm-project/compiler-rt/lib/msan/msan_interceptors.cpp:218:3 #1 0x56115898e06b in _PyMem_RawFree third_party/python_runtime/v3_11/Objects/obmalloc.c:154:5 #2 0x56115898f6ad in PyObject_Free third_party/python_runtime/v3_11/Objects/obmalloc.c:769:5 #3 0x561158271bcc in PyObject_GC_Del third_party/python_runtime/v3_11/Modules/gcmodule.c:2407:5 #4 0x7f21224b070c in pybind11_object_dealloc third_party/pybind11/include/pybind11/detail/class.h:483:5 #5 0x5611589c2ed0 in subtype_dealloc third_party/python_runtime/v3_11/Objects/typeobject.c:1463:5 ... ``` * IncludeCleaner fixes (Google toolchain). * Restore `type->tp_call = pybind11_meta_call;` for PyPy only. * pytest.skip("ensure_base_init_functions_were_called() does not work with PyPy and Python `type` as metaclass") * Do not intercept our own `tp_init` function (`pybind11_object_init`). * Add `derived_tp_init_registry` weakref-based cleanup. * Replace `assert()` with `if` to resolve erroneous `lambda capture 'type' is not used` diagnostics (many CI jobs; seems to be a clang issue). * Add `derived_tp_init_registry()->count(type) == 0` condition. * Changes based on feedback from @rainwoodman * Use PYBIND11_INIT_SAFETY_CHECKS_VIA_* macros, based on suggestion from @rainwoodman
…e (#30056) * Snapshot of pybind#4762 applied to pywrapcc * Universal `bases.size() != vhs.size()` (not as `assert()`) * Revert "Universal `bases.size() != vhs.size()` (not as `assert()`)" This reverts commit 4c2407d17e982e1512f43ad89bf8752c0d2c7fe0. * Streamline implementation and avoid unsafe `reinterpret_cast<instance *>()` introduced with PR pybind#2152 The `reinterpret_cast<instance *>(self)` is unsafe if `__new__` is mocked, which was actually found in the wild: the mock returned `None` for `self`. This was inconsequential because `inst` is currently cast straight back to `PyObject *` to compute `all_type_info()`, which is empty if `self` is not a pybind11 `instance`, and then `inst` is never dereferenced. However, the unsafe detour through `instance *` is easily avoided and the updated implementation is less prone to accidents while debugging or refactoring. * Fix actual undefined behavior exposed by previous changes. It turns out the previous commit message is incorrect, the `inst` pointer is actually dereferenced, in the `value_and_holder` ctor here: https://github.com/pybind/pybind11/blob/f3e0602802c7840992c97f4960515777cad6a5c7/include/pybind11/detail/type_caster_base.h#L262-L263 ``` 259 // Main constructor for a found value/holder: 260 value_and_holder(instance *i, const detail::type_info *type, size_t vpos, size_t index) 261 : inst{i}, index{index}, type{type}, 262 vh{inst->simple_layout ? inst->simple_value_holder 263 : &inst->nonsimple.values_and_holders[vpos]} {} ``` * Add test_mock_new()
…ting python object fixes: pybind#6153 Objects initialized with `cls.__new__(cls)` (`cls` is a pybind11 bound type). Will not have the C++ object allocated. When hitting `load_value` storage is allocated but not initialized, calling a virtual method will load a garbage vptr and segfault. This is similar to pybind#2152, but the guard in metaclass `__call__` is not triggered when using `__new__`. Protect against giving a pointer to garbage in all cases except the `__init__` + `__setstate__` path. Authored with claude
* fix: Guard against using a uninitialized value after `__new__` allocating python object fixes: #6153 Objects initialized with `cls.__new__(cls)` (`cls` is a pybind11 bound type). Will not have the C++ object allocated. When hitting `load_value` storage is allocated but not initialized, calling a virtual method will load a garbage vptr and segfault. This is similar to #2152, but the guard in metaclass `__call__` is not triggered when using `__new__`. Protect against giving a pointer to garbage in all cases except the `__init__` + `__setstate__` path. Authored with claude * fix: free lazily allocated storage on failed init and only permit lazy allocation for old-style constructors If an old-style placement-new `__init__`/`__setstate__` failed after `self` was loaded, the lazily allocated storage stayed behind with a null-holder instance, so the uninitialized-value guard never fired again and later use read uninitialized memory. `instance_construction_scope` now tracks the constructor's `value_and_holder` and frees storage that was lazily allocated during a construction that did not complete. Also arm the scope only when the overload chain contains an old-style constructor. New-style constructors receive `self` directly and never need lazy allocation, so reentrant loads of the half-built instance now raise `ValueError` instead of handing out uninitialized storage. Assisted-by: ClaudeCode:claude-fable-5 Claude-Session: https://claude.ai/code/session_01TQXCSykMn5EL7sc6VgTUTC * fix: isolate old-style constructor storage Track construction per value-and-holder, grant a one-shot loader-frame permission only to the exact legacy constructor self conversion, and keep its raw storage private until the native callback returns. Reject reentrant, nested, cross-base, and cross-thread loads while preserving overload fallback, failure cleanup, pickle setstate callbacks, and repeated initialization behavior. * test: skip constructor thread test on Emscripten * fix: bump internals version to 13 The new detail::instance construction state has cross-DSO semantics that internals-v12 modules do not understand. Isolate the incompatible domains for v3.2.0 and document that future structural or semantic instance changes require another bump. * Revert "fix: bump internals version to 13" This reverts commit 14e32ae. * fix: recover from legacy constructor storage collisions * test: fix collision subprocess imports * refactor: simplify old-style constructor storage tracking The loader frame already identifies the constructor candidate, so the one-shot `self` permission only needs a frame match and a claimed flag. This removes both argument guard classes, the changes to cast.h, and the per-call TLS lookups they added. Also: - Hoist deallocate_instance_value to a detail free function and use it from instance_construction_scope. - Take the dispatcher's constructor lock before the construction scope and drop the nested critical sections it made redundant. - Keep the non-constructor path inline: the loader destructor checks for storage before the out-of-line cleanup, and the construction scope defaults to not started. - Commit old-style storage once in cpp_function::initialize, gated on is_constructor. - Share one __index__ probe across the reentrancy tests and turn the subprocess script into a plain function. Assisted-by: ClaudeCode:claude-fable-5-1 * test: reject later self alias during old-style init * fix: restrict old-style constructor self permission to self's own load phase The one-shot `self` permission granted by `loader_life_support` matched only the frame and the value slot, not the *phase* of the load. The slot is identified by the instance, so any later argument that aliases the same, still-unconstructed `self` matched too, consumed the reservation, and reached C++ over raw storage. Track the frame's phase instead: authorize the load of positional argument 0 (the typed-`self` variant) and casts performed from within the C++ callable (the legacy `py::object`-self variant), and deny every load during conversion of positional arguments >= 1. `argument_loader` reports the argument index, and the dispatcher flips the frame to the callable phase once loading is done. The frame pointer is resolved once in the dispatcher, where `is_constructor` is known, so no non-constructor call pays a thread-local lookup. This restores the restriction that 89a5f72 dropped, re-enabling the regression test added in 7a7e9f3, and adds three more tests. Both new negative tests assert the decisive observable rather than only that the callback was entered, and both use types chosen so that a build where the guard has regressed reports an assertion failure instead of crashing during teardown: - A later argument typed as a *base* of the class under construction. It shares the value slot, so it matched, and the reservation was then sized from the base's `type_info`: 16 bytes for a 144-byte derived object. Because the claim is one-shot it also denied `self` its own storage, so the callback could not placement-new at all; the value was nevertheless committed, and destroying it ran a virtual destructor over never-constructed memory. The test asserts that no reservation was made at the base's size. - The still-unconstructed `self` reached through a container argument. Here `stl.h`'s element caster copy-constructs, so the read of uninitialized memory happens inside pybind11 and no binding author can guard against it. The test counts copy constructions whose source was raw storage and asserts zero; the instrumented copy constructor does not read its source, so the test itself performs no uninitialized read. - A positive test pinning the case that must keep working: a later argument that is a different, already-constructed instance of the same class. Assisted-by: ClaudeCode:claude-opus-5 * style: pre-commit fixes * style: clang-tidy fixes The Clang-Tidy job failed on two `modernize-use-default-member-init` diagnostics in tests/test_class.cpp, both introduced by this branch: tests/test_class.cpp:167:18: error: use default member initializer for 'payload' [modernize-use-default-member-init,-warnings-as-errors] tests/test_class.cpp:177:9: error: use default member initializer for 'value' [modernize-use-default-member-init,-warnings-as-errors] Applied exactly the replacements clang-tidy emitted: - `AliasStealDerived::payload` gains a `{}` default member initializer and drops `payload{}` from the constructor initializer list. Both value-initialize the array. - `ContainerAliasItem::value` gains a `{-1}` default member initializer and the copy constructor drops `value(-1)`. The converting constructor keeps `value(v)`, which overrides the default, so both constructors still produce the values the container-alias test asserts on. No behavior change; clang-tidy is not installed locally, so the fix-its were transcribed from the CI diagnostic rather than auto-applied, and the translation unit was compiled clean at -std=c++17 with the CI warning set. Assisted-by: ClaudeCode:claude-opus-5 * fix: GraalPY exceptions Both GraalPy jobs failed on the same assertion in the legacy-v12 collision test: tests/test_class.py:469: assert stats() == (3, 3, constructed + 1, constructed + 1) That is the final check, reached after `del obj` and two `gc.collect()` calls. GraalPy is not refcounted and does not guarantee finalization from `gc.collect()`, so the destruction counters lag and the assertion fails while every preceding assertion in the loop passes. Gate only that assertion behind `if not env.GRAALPY:`. This follows the existing convention in the suite, where GC-timing-dependent checks are exempted on GraalPy with the same "Cannot reliably trigger GC" reason (test_call_policies.py, test_callbacks.py, test_class_sh_trampoline_shared_ptr_cpp_arg.py, and others). Nothing this branch introduces stops being tested on GraalPy. The gated line only observes ordinary teardown of a normal, fully constructed retry object. The rollback properties the test exists for are pinned by the assertions above it, which still run everywhere: after rollback both collision allocations are freed with no spurious destruction, and after the retry exactly one allocation is live with the private value's destructor having run neither early nor twice. Assisted-by: ClaudeCode:claude-opus-5 * test: pin the two gaps identified in the load-phase review Adds coverage for the two limitations called out in the review of the load-phase restriction. Both tests pass, pinning today's behavior; both fail against `master`'s headers, which is what makes them meaningful. test_old_style_init_value_error_hides_later_overload Two old-style candidates take the same two Python arguments. The first one's argument 1 is the `self` alias that the construction guard rejects; the second matches the same call and constructs. The guard reports rejection with `value_error`, and only `reference_cast_error` becomes PYBIND11_TRY_NEXT_OVERLOAD, so the throw escapes the overload loop and the second candidate is never attempted. Verified counterfactual, same test files built against master's headers: master reaches the second candidate and constructs (`entered == ["second candidate entered"]`); here the call raises ValueError with `entered == []`. Note this is a new trigger for pre-existing behavior rather than a new behavior: master's casters already throw `value_error` from load paths with the same non-fallthrough consequence. test_old_style_init_callable_phase_grant_is_not_self_specific While the callable runs, the one-shot grant is keyed on the value slot, not on the `self` handle, so a cast of `stash[0]` claims the reservation and the genuine `self` cast then fails. Narrowing the grant to "a cast of the `self` object" would not close this: the claiming cast targets the same Python object as `self`, so the two are indistinguishable at cast time. Verified counterfactual: on master both casts succeed (`["stash cast claimed the reservation", "self cast succeeded"]`) because every load lazily allocates. The one-shot reservation is therefore a narrowing of master's behavior, and this gap is the residue rather than a regression. Neither callback inspects the reference it obtains over storage whose lifetime has not begun, so the tests themselves stay free of undefined behavior. Both verify the object is still retryable afterwards. Assisted-by: ClaudeCode:claude-opus-5 * perf: only test the old-style frame pointer where the phase can change Addresses the review suggestion to stop paying the null check once per argument. The literal form suggested, `I == 0 &&`, is not safe: `begin_argument_load` is what moves the frame from `self_argument` to `later_argument`, so skipping it for arguments 1 and up leaves the phase at `self_argument` for the whole argument list. That re-opens exactly the hole 955cb19 closed. It regresses four tests: test_old_style_init_does_not_authorize_later_self_alias test_old_style_init_does_not_authorize_base_typed_later_alias test_old_style_init_does_not_authorize_self_alias_inside_container test_old_style_init_value_error_hides_later_overload Gate on `I < 2` instead. The phase only changes at argument 0 and argument 1; from argument 2 on it is already `later_argument`, so those arguments need no call and no test. Two checks per call rather than one, but it is the minimum that preserves the invariant. All 55 tests pass. `I` is a template parameter, so no `if constexpr` is needed and none can be used: pybind11 still supports C++11 and `if constexpr` is a C++17 extension there. A plain `if` on a constant condition already folds completely. clang -O2, the `I == 5` instantiation of a reduction of this function tail-calls straight through with no pointer test emitted, while `I == 0` and `I == 1` keep theirs. MSVC C4127 (constant conditional) is already disabled file-wide at the top of cast.h, and the header compiles clean at -std=c++11/14/17/20 with -Wall -Wextra -Wpedantic -Wconversion -Werror. Assisted-by: ClaudeCode:claude-opus-5 * docs: describe status_value_constructing with the other status bits The non-simple layout comment enumerated status_holder_constructed and status_instance_registered but not status_value_constructing, which was added alongside them. Addresses the review comment on that block. Also states what the bit means for readers of the value pointer: while it is set, the pointer must not be treated as denoting a live C++ object. That is the invariant the rest of this change depends on, and the status byte is where someone will look for it. Comment-only. Longest line is 94 columns, within the 99-column limit, so clang-format does not reflow it. Assisted-by: ClaudeCode:claude-opus-5 * style: pre-commit fixes * style: clang-tidy/format * fix: narrow uninitialized-instance guard to direct misuse Return to the minimal scope needed for #6153: ordinary loads of a wrapper with no constructed C++ value raise ValueError, while overload chains containing deprecated placement-new constructors retain their historical lazy allocation. Failed old-style construction also frees lazily allocated storage so the object remains guarded and retryable. Remove the expanded per-value construction protocol, including private candidate storage, argument and callable phases, cross-thread locking, and stale-v12 collision recovery. Those mechanisms attempted to make deprecated placement-new construction safe under reentry rather than fixing direct __new__ misuse. Accordingly, remove tests requiring special handling for later arguments (both distinct instances and self aliases), base-typed and container aliases, nested initialization, Python multiple-inheritance bases, concurrent access, mixed old/new overloads, and old-style __setstate__ reentry. Also remove tests pinning protocol-specific overload fallthrough, callable-phase grants, and legacy-v12 collision cleanup. Keep focused coverage for direct __new__ misuse, reentry during new-style construction, deprecated __init__/__setstate__ compatibility, failed-construction cleanup, and successful retry. * test: characterize old-style reentrant load limitation Keep one pointer-only probe for the historical broad lazy-allocation window. It records the known hazard that a reentrant load can expose a pointer to unconstructed storage, without inspecting or dereferencing that storage. The cleanup and retry assertions remain in place so the behavior retained by the minimal fix stays covered. * docs: document deprecated placement-new limitations Explain that the compatibility window spans an entire constructor overload chain and can expose unconstructed storage during reentrant conversion or callbacks, nested initialization, Python multiple inheritance, or concurrent access. Recommend new-style constructor and pickle APIs, and add source-level references at the compatibility flag and scope so future changes encounter the accepted limitations and rationale before attempting to narrow the window. --------- Co-authored-by: Henry Schreiner <henryfs@princeton.edu> Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com> Co-authored-by: Andrew M. James <ajames@openteams.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Forgetting to call
__init__in inherited classes is one of the biggest issues my users run into (and it's frustrating because it typically causes a segfault), I'd love to see this merged. It would make pybind11 so much more usable!... this probably causes a minor performance impact when creating new objects. I would expect it to be in the noise, though I haven't looked at the timing yet.