-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
base: master
Are you sure you want to change the base?
Conversation
If idea is ok, I will fix failing builds |
TL;DR:
To explain to myself(!): Why do we need the production code changes? We have existing tests for
However, In contrast, your template <typename V = void>
V *&value_ptr() const {
- return reinterpret_cast<V *&>(vh[0]);
+ if constexpr (std::is_const_v<V>) {
+ return reinterpret_cast<V *&>(const_cast<const void *&>(vh[0]));
+ } else {
+ return reinterpret_cast<V *&>(vh[0]);
+ }
} I think that'd be OK (with a C++11-compatible implementation) ... if you can provide a convincing use case: Looking at your test code, I believe this leaks:
I don't see a corresponding Could you please provide a more realistic use case? |
To be honest, this my real use case. I have some number of objects, that are allocate and are alive until program exits, so for them I use StaticPtr wrapper, and they are const because i share them across threads |
First impression: That's a pretty involved way of dealing with the situation. Are you aware of this? I tested this locally: #include "pybind11_tests.h"
#include <string>
class MyData {
public:
static std::unique_ptr<MyData, py::nodelete> create(std::string name) {
return std::unique_ptr<MyData, py::nodelete>(new MyData(std::move(name)));
}
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, std::unique_ptr<MyData, py::nodelete>>(m, "Data")
.def(py::init([](const std::string &name) { return MyData::create(name); }))
.def_property_readonly("name", &MyData::getName);
} Could that pattern work for your real use case? |
I forgot to add, you could just copy this into your codebase, under your own namespace, if you want keep your C++ code independent from pybind11: struct nodelete {
template <typename T>
void operator()(T *) {}
}; |
Using unique_ptr in my code base would not work because it is not copyable. I can do something like this class MyData {
public:
static StaticPtr<MyData> getOrCreate(const std::string &name) {
// here should be logic to get or create a pointer
const MyData *ptr = new MyData(std::string(name));
return StaticPtr(ptr);
}
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, std::unique_ptr<MyData, py::nodelete>>(m, "Data")
.def(py::init([](const std::string &name) {
auto ptr = MyData::getOrCreate(name);
return std::unique_ptr<MyData, py::nodelete>(const_cast<MyData *>(ptr.get()));
}))
.def_property_readonly("name", &MyData::getName);
} but then i won't be able to easily export some functions that use |
I tested this locally: #include "pybind11_tests.h"
#include <string>
class MyData {
public:
static std::shared_ptr<MyData> create(std::string name) {
return std::shared_ptr<MyData>(new MyData(std::move(name)), [](MyData *) {});
}
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, py::smart_holder>(m, "Data")
.def(py::init([](const std::string &name) { return MyData::create(name); }))
.def_property_readonly("name", &MyData::getName);
} The more I think about this, the more doubts I have about that pattern. What's the purpose of a factory function that always leaks? Make it unobvious that you're leaking memory? |
It always leaks just for this example; in reality, it first searches for existing pointer associated with this name, if it fails, creates new one. Using shared_pointer in my code base is also not an option, because it does extra ref counting and takes twice the space. If it will be more convincing, let's imagine that my use case is this. template <class T>
class MySmartPointer {
public:
// regular shared_ptr methods
const T * get() const { return data_; }
private:
const T * data_ = nullptr;
std::atomic<uint32_t> * counter_ = nullptr;
} |
I want to look in two directions then:
Almost identical, just a couple template <class T>
class MySmartPointer {
public:
// regular shared_ptr methods
/*const*/ T * get() const { return data_; }
private:
/*const*/ T * data_ = nullptr;
std::atomic<uint32_t> * counter_ = nullptr;
} Then: |
@@ -33,7 +33,11 @@ struct value_and_holder { | |||
|
|||
template <typename V = void> | |||
V *&value_ptr() const { | |||
return reinterpret_cast<V *&>(vh[0]); | |||
if constexpr (std::is_const_v<V>) { | |||
return reinterpret_cast<V *&>(const_cast<const void *&>(vh[0])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we do this unconditionally (without the std::is_const_v<V>
test)?
To keep the code simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no it will cause errors like this error: ‘reinterpret_cast’ from type ‘const void**’ to type ‘void**’ casts away qualifiers
…n code changes significantly. For background see: https://claude.ai/share/4085d9ab-a859-44cc-bb56-450e472f817a
I just pushed fc8ca60 Copying the link from there: https://claude.ai/share/4085d9ab-a859-44cc-bb56-450e472f817a |
tests/test_const_smart_ptr.cpp
Outdated
class MyData { | ||
public: | ||
static StaticPtr<MyData> create(std::string name) { | ||
return StaticPtr<MyData>(new MyData(std::move(name))); |
There was a problem hiding this comment.
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.
For completeness, I also quizzed ChatGPT: I still have doubts about your Could you please rename |
I think |
std::string name_; | ||
}; | ||
} // namespace const_only_smart_ptr | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can put
using namespace const_only_smart_ptr;
here. That's local to this translation unit (no danger of name clashes). The only important part is that the definitions are inside a namespace.
I tried that locally, to be sure what I'm writing actually works, then pushed it.
It's now test_const_only_smart_ptr
, which is what we usually have here.
ChatGPT is suggesting the below. Could you please adapt? Here is the full conversation (which also explains why I think it's good to go the extra mile, even though it's only a test): https://chatgpt.com/share/684768ba-fc1c-8008-bdbf-f31844b3ec4d template <class T>
class non_sync_const_shared_ptr {
public:
non_sync_const_shared_ptr() = default;
explicit non_sync_const_shared_ptr(T *ptr) : ptr_(ptr) {
if (ptr_) {
ref_count_ = new std::size_t(1);
}
}
non_sync_const_shared_ptr(const non_sync_const_shared_ptr &other)
: ptr_(other.ptr_), ref_count_(other.ref_count_) {
if (ref_count_) {
++(*ref_count_);
}
}
non_sync_const_shared_ptr &operator=(const non_sync_const_shared_ptr &other) {
if (this != &other) {
release();
ptr_ = other.ptr_;
ref_count_ = other.ref_count_;
if (ref_count_) {
++(*ref_count_);
}
}
return *this;
}
~non_sync_const_shared_ptr() {
release();
}
const T *get() const { return ptr_; }
const T &operator*() const { return *ptr_; }
const T *operator->() const { return ptr_; }
explicit operator bool() const { return ptr_ != nullptr; }
private:
void release() {
if (ref_count_) {
--(*ref_count_);
if (*ref_count_ == 0) {
delete ptr_;
delete ref_count_;
}
}
}
const T *ptr_ = nullptr;
std::size_t *ref_count_ = nullptr;
}; |
claude.ai has it's own opinion and suggested fixes: https://claude.ai/share/8eaaf0d9-04ba-48cd-a858-ac0f13ce97bb I'll stop here. I hope it's pretty easy for you to pick out what's valuable. |
Sorry, the test is getting way too big now (partially my fault). Offline @henryiii suggested simply adding something like a minimal |
Description
In my project, I am mainly using smart pointers that can hold only pointers to const, but Pybind does not allow them to be passed to Python. Here is a small fix to allow this behavior
Suggested changelog entry: