Skip to content

[BUG]: static_pointer_cast where dynamic_pointer_cast is needed #5989

@leakec

Description

@leakec

Required prerequisites

What version (or hash if on master) of pybind11 are you using?

3.0.2

Problem description

If a class is using virtual inheritance and its holder type is a shared pointer, then any method using that class fails due to the static_pointer_cast in pybind11/detail/holder_caster_foreign_helpers.h.

This used to work in pybind11 3.0.1, so I believe this is a new bug introduced in version 3.0.2.

Example code tarball for convenience:
example.tar.gz

Reproducible example code

Note, all the files listed below are part of the example.tar.gz attached above. Feel free to download and untar it if you would like to avoid copy-pasting all the files below.

example.h

#pragma once

#include <memory>

class Base: public std::enable_shared_from_this<Base> {
  public:
    Base();
    virtual ~Base();
    static std::shared_ptr<Base> create();
    virtual void method();
};

class Derived: public Base {
  public:
    using Base::Base;
    ~Derived();
    static std::shared_ptr<Derived> create();
    void method() override;
};

class Derived2: public virtual Derived {
  public:
    using Derived::Derived;
    ~Derived2();
    static std::shared_ptr<Derived2> create();
    void method() override;
    void method2(const std::shared_ptr<Derived2>& d2);
};

example.cc

#include "example.h"
#include <iostream>

Base::Base() {}
Base::~Base() {}
void Base::method() {
    std::cout << "Base" << std::endl;
}
std::shared_ptr<Base> Base::create() {
    return std::make_shared<Base>();
}

Derived::~Derived(){}
void Derived::method() {
    std::cout << "Derived" << std::endl;
}
std::shared_ptr<Derived> Derived::create() {
    return std::make_shared<Derived>();
}

Derived2::~Derived2(){}
void Derived2::method() {
    std::cout << "Derived2" << std::endl;
}
std::shared_ptr<Derived2> Derived2::create() {
    return std::make_shared<Derived2>();
}
void Derived2::method2(const std::shared_ptr<Derived2>& d2) {
    d2->method();
}

example_Py.cc

#include <pybind11/pybind11.h>
#include "example.h"
namespace py = pybind11;

PYBIND11_MODULE(example_Py, m) {
    py::class_<Base, std::shared_ptr<Base>>(m, "Base")
        .def(py::init<>(&Base::create))
        .def("method", &Base::method);

    py::class_<Derived, Base, std::shared_ptr<Derived>>(m, "Derived")
        .def(py::init<>(&Derived::create));

    py::class_<Derived2, Derived, std::shared_ptr<Derived2>>(m, "Derived2")
        .def(py::init<>(&Derived2::create))
        .def("method2", &Derived2::method2, py::arg("d2"));
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.25)
project(example)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

find_package(Python REQUIRED COMPONENTS Interpreter Development)

# Find pybind11 using the path from Python
execute_process(
    COMMAND ${Python_EXECUTABLE} -c "import pybind11; import pathlib; print(pathlib.Path(pybind11.__file__).parent / \"share\" / \"cmake\" / \"pybind11\")"
    OUTPUT_VARIABLE pybind11_DIR
    OUTPUT_STRIP_TRAILING_WHITESPACE
)
find_package(pybind11 3.0 REQUIRED CONFIG)

# Build library
add_library(example example.cc)

# Add python module
pybind11_add_module(example_Py example_Py.cc)
target_link_libraries(example_Py PRIVATE example)

# Install the module
install(TARGETS example_Py DESTINATION .)

test.py

import example_Py

b = example_Py.Base()
b.method()

d = example_Py.Derived()
d.method()

d2 = example_Py.Derived2()
d2.method()
d2.method2(d2)

run

#!/bin/bash

# Clean old build
rm -rf build *.so

# Make and install
cmake -B build -DCMAKE_INSTALL_PREFIX=`pwd` .
cmake --build build -j 10
cmake --install build

# Test
python test.py

If you create the files listed above and run ./run, everything works as expected for pybind11 3.0.1, but fails to compile example_Py.cc for pybind11 3.0.2. The failure to compile happens because of method2 on Derived2. Without this method, everything passes. This is where a static_pointer_cast is used to try to convert a Base to a Derived2, but a dynamic_pointer_cast is needed since virtual inheritance is involved.

Is this a regression? Put the last known working version here if it is.

3.0.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    triageNew bug, unverified

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions