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
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ set(PYBIND11_TEST_FILES
test_class_sh_unique_ptr_member
test_class_sh_virtual_py_cpp_mix
test_const_name
test_const_smart_ptr
test_constants_and_functions
test_copy_move
test_cpp_conduit
Expand Down
40 changes: 40 additions & 0 deletions tests/test_const_smart_ptr.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include "pybind11/cast.h"
#include "pybind11_tests.h"

#include <string>

template <class T>
class StaticPtr {
public:
explicit StaticPtr(const T *ptr) : ptr_(ptr) {}

const T *get() const { return ptr_; }

const T &operator*() const { return *ptr_; }
const T *operator->() const { return ptr_; }

private:
const T *ptr_ = nullptr;
};

PYBIND11_DECLARE_HOLDER_TYPE(T, StaticPtr<T>, true)

class MyData {
public:
static StaticPtr<MyData> create(std::string name) {
return StaticPtr<MyData>(new MyData(std::move(name)));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming the CI still passes, I'm fine with the production code changes.

However, we need a test case that does not leak. I wouldn't want to surprise someone (years) later who's trying to use this test for leak detection while working on a refactoring, and is plausibly unaware that the test code has leaks. It's also possible that future tooling will flag this code.

}

const std::string &getName() const { return name_; }

private:
explicit MyData(std::string &&name) : name_(std::move(name)) {}

std::string name_;
};

TEST_SUBMODULE(const_module, m) {
py::class_<MyData, StaticPtr<MyData>>(m, "Data")
.def(py::init([](const std::string &name) { return MyData::create(name); }))
.def_property_readonly("name", &MyData::getName);
}
11 changes: 11 additions & 0 deletions tests/test_const_smart_ptr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from __future__ import annotations

import pytest

asyncio = pytest.importorskip("asyncio")
m = pytest.importorskip("pybind11_tests.const_module")


def test_const_smart_ptr():
cons = m.Data("my_name")
assert cons.name == "my_name"
Loading