Skip to content

fix: add support for const-only smart pointers #5718

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 11, 2025
11 changes: 8 additions & 3 deletions include/pybind11/detail/init.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class type_caster<value_and_holder> {

PYBIND11_NAMESPACE_BEGIN(initimpl)

inline void no_nullptr(void *ptr) {
inline void no_nullptr(const void *ptr) {
if (!ptr) {
throw type_error("pybind11::init(): factory function returned nullptr");
}
Expand All @@ -61,7 +61,7 @@ bool is_alias(Cpp<Class> *ptr) {
}
// Failing fallback version of the above for a no-alias class (always returns false)
template <typename /*Class*/>
constexpr bool is_alias(void *) {
constexpr bool is_alias(const void *) {
return false;
}

Expand Down Expand Up @@ -167,7 +167,12 @@ void construct(value_and_holder &v_h, Holder<Class> holder, bool need_alias) {
"is not an alias instance");
}

v_h.value_ptr() = ptr;
// Cast away constness to store in void* storage.
// The value_and_holder storage is fundamentally untyped (void**), so we lose
// const-correctness here by design. The const qualifier will be restored
// when the pointer is later retrieved and cast back to the original type.
// This explicit const_cast makes the const-removal clearly visible.
v_h.value_ptr() = const_cast<void *>(static_cast<const void *>(ptr));
v_h.type->init_instance(v_h.inst, &holder);
}

Expand Down
20 changes: 20 additions & 0 deletions tests/test_smart_ptr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ class unique_ptr_with_addressof_operator {
T **operator&() { throw std::logic_error("Call of overloaded operator& is not expected"); }
};

// Simple custom holder that imitates smart pointer, that always stores cpointer to const
template <class T>
class const_only_shared_ptr {
std::shared_ptr<const T> ptr_;

public:
const_only_shared_ptr() = default;
explicit const_only_shared_ptr(const T *ptr) : ptr_(ptr) {}
const T *get() const { return ptr_.get(); }

private:
// for demonstration purpose only, this imitates smart pointer with a const-only pointer
};

// Custom object with builtin reference counting (see 'object.h' for the implementation)
class MyObject1 : public Object {
public:
Expand Down Expand Up @@ -283,6 +297,7 @@ struct holder_helper<ref<T>> {

// Make pybind aware of the ref-counted wrapper type (s):
PYBIND11_DECLARE_HOLDER_TYPE(T, ref<T>, true)
PYBIND11_DECLARE_HOLDER_TYPE(T, const_only_shared_ptr<T>, true)
// The following is not required anymore for std::shared_ptr, but it should compile without error:
PYBIND11_DECLARE_HOLDER_TYPE(T, std::shared_ptr<T>)
PYBIND11_DECLARE_HOLDER_TYPE(T, huge_unique_ptr<T>)
Expand Down Expand Up @@ -397,6 +412,11 @@ TEST_SUBMODULE(smart_ptr, m) {
m.def("print_myobject2_4",
[](const std::shared_ptr<MyObject2> *obj) { py::print((*obj)->toString()); });

m.def("make_myobject2_3",
[](int val) { return const_only_shared_ptr<MyObject2>(new MyObject2(val)); });
m.def("print_myobject2_5",
[](const const_only_shared_ptr<MyObject2> &obj) { py::print(obj.get()->toString()); });

py::class_<MyObject3, std::shared_ptr<MyObject3>>(m, "MyObject3").def(py::init<int>());
m.def("make_myobject3_1", []() { return new MyObject3(8); });
m.def("make_myobject3_2", []() { return std::make_shared<MyObject3>(9); });
Expand Down
7 changes: 7 additions & 0 deletions tests/test_smart_ptr.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,10 @@ def test_move_only_holder_caster_shared_ptr_with_smart_holder_support_enabled():
assert (
m.return_std_unique_ptr_example_drvd() == "move_only_holder_caster_traits_test"
)


def test_const_only_holder(capture):
o = m.make_myobject2_3(4)
with capture:
m.print_myobject2_5(o)
assert capture == "MyObject2[4]\n"
Loading