pybind / pybind/pybind11

C++ object is destructed before it can be used, when returned as a `shared_ptr` and using default holder type

Open
#1,215 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
18k
Forks
2.3k
Avg merge
5d 17h
Merged PRs (30d)
10

Description

Issue description

When returning a shared_ptr to a bound object from a C++ function, the object seems to be immediately destructed, although it continues to be "usable" from Python, but this is dangerous, because it is a "dangling" object.

What I think would be more appropriate in this case:

  1. Give an error message stating that a function may not return shared_ptr of an object when it's holder type is the default one.
  2. If that is not possible, at least let the function return None, this is way less dangerous.

Note that the object ends up being destructed twice!

Reproducible example code

#include <pybind11/pybind11.h>
#include <memory>
#include <iostream>

namespace py = pybind11;

class BindingsTest {
public:
    static size_t constructor_called;
    static size_t destructor_called;

    BindingsTest() { constructor_called += 1; status = "OK";  }
    ~BindingsTest() { destructor_called += 1; status = "INVALID"; }
    BindingsTest(BindingsTest const&) = delete;
    const char* get_status() const { return status; }

protected:
    const char* status;
    bool destructor_already_called;
};

size_t BindingsTest::constructor_called = 0;
size_t BindingsTest::destructor_called = 0;

std::shared_ptr<BindingsTest> create_bindings_test() {
    return std::make_shared<BindingsTest>();
}

size_t get_constructor_called() { return BindingsTest::constructor_called; }
size_t get_destructor_called() { return BindingsTest::destructor_called; }
void reset_called() {
    BindingsTest::constructor_called = 0;
    BindingsTest::destructor_called = 0;
}

PYBIND11_MODULE(_pb11pg, m) {
    m.doc() = "pybind11 testing module";

    py::class_<BindingsTest>(m, "BindingsTest")
        .def("get_status", &BindingsTest::get_status);
    m.def("create_bindings_test", &create_bindings_test);
    m.def("reset_called", &reset_called);
    m.def("get_constructor_called", &get_constructor_called);
    m.def("get_destructor_called", &get_destructor_called);
}
def test_bindings_problem():
    import _pb11pg
    _pb11pg.reset_called()

    x = _pb11pg.create_bindings_test()

    # Destructor was already called here
    assert x.get_status() == 'OK'
    assert _pb11pg.get_constructor_called() == 1
    assert _pb11pg.get_destructor_called() == 0

    x = None
    assert _pb11pg.get_constructor_called() == 1
    assert _pb11pg.get_destructor_called() == 1

Workaround

Use shared_ptr as a holder type, but the problem is that it is very hard to find that this is the issue causing the segfaults and unexpected behaviours:

py::class_<BindingsTest, shared_ptr<BindingsTest>> instead of py::class_<BindingsTest>

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the py::class_ holder declaration and the shared_ptr return path illustrated in the issue, then reproduce the behavior using the provided C++ module and Python test. Done means the returned object is not dangling or destructed twice, with the reported constructor and destructor counts remaining consistent.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
api
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.