Skip to content
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

Inheritance with non-exposed base class #10

Closed
YannickJadoul opened this issue Nov 13, 2015 · 12 comments
Closed

Inheritance with non-exposed base class #10

YannickJadoul opened this issue Nov 13, 2015 · 12 comments

Comments

@YannickJadoul
Copy link
Collaborator

I came across this project, yesterday, and I'd like to say, I really like the project, improving on Boost.Python's environment and API quirks, and I'd like to use it in my C++-to-Python wrapper.

But as I was looking, yesterday, I came across a problem. I was trying to expose a leaf class of a large hierarchy, and encountered a problem when calling a method of the base class that wasn't overridden in the exposed class.

Here is the stripped down example of what I was trying to do:

#include <iostream>

class A
{
public:
    A() : x(1) {}
    virtual void f() { std::cout << "A::f" << std::endl; }
    virtual void g() { std::cout << "A::g" << std::endl; }
            void h() { std::cout << "A::h" << std::endl; }
            void i() { std::cout << "A::i" << std::endl; }

    int x;
};

class B : public A
{
public:
    virtual void f() { std::cout << "B::f" << std::endl; }
            void h() { std::cout << "B::h" << std::endl; }
};

#include "pybind11/pybind11.h"

namespace py = pybind11;

PYBIND11_PLUGIN(test)
{
    py::module m("test");

//  py::class_<A> a(m, "A");

    py::class_<B>(m, "B"/*, a*/)
        .def(py::init<>())
        .def("f", &B::f)
        .def("g", &B::g)
        .def("h", &B::h)
        .def("i", &B::i)
        .def_readwrite("x", &B::x)
    ;
}

If I then import the resulting library in Python, and try to use it, I get the following:

>>> import test
>>> b = test.B()
>>> b.i()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Incompatible function arguments. The following argument types are supported:
    1. (A) -> None

The same goes for calling 'g' and accessing 'x'.

I tried the same with Boost.Python, and it does not seem to have this problem. (Which is a pitty, 'cause again, I really like this slim header-only library, instead of Boost ;-) )

EDIT: Oh, yes, almost forgot. If you remove the 2 comments, and expose the A class and the fact that is is B's base class, everything works as it should.

@wjakob
Copy link
Member

wjakob commented Nov 14, 2015

This is kind of a unusual use case: why expose a child class with functions from parents, without being willing to at least create a very rough map with stubs of the parent hierarchy?

Changing the library to address this will require changes that significantly complicate the interface. IMHO, the failure of pybind11 to accept this kind of thing is preferable to a more complex implementation.

@wjakob
Copy link
Member

wjakob commented Nov 14, 2015

also, by the way: if you absolutely need to do this, you can always always do a cast yourself using a trivial lambda function:

        .def("i", [](B &b) { b.i(); })

@YannickJadoul
Copy link
Collaborator Author

Well, I don't want to expose the whole hierarchy as API for the Python library (I'm trying to wrap code that isn't mine). The Python user should not really know about all the underlying classes (i.e. A), as far as I'm concerned, but just use the leaf class (B). And it just struck me that this construction did not work, while the extension module nicely compiles.

But I do not have a view on the internals, and must admit/understand that it is not worth the complication of the pybind11 interface. But do you prefer the way it is now, or would you change it if there was a (reasonably) simple solution?

@wjakob
Copy link
Member

wjakob commented Nov 15, 2015

If there was a simple and clean solution, I would accept it. I don't see a simple way to do it though.

@mlund
Copy link

mlund commented Nov 15, 2015

I have a related issue for a class inheriting from Eigen:

struct PointBase : public Eigen::Vector3d {...};

where in c++ I use baseclass functions for read/write access:

PointBase p;
p.x()=1.0
cout << p.x()

Any suggestions how to expose this? Directly to numpy array?

@wjakob
Copy link
Member

wjakob commented Nov 16, 2015

You could directly return a numpy array or wrap the eigen types + extra hierarchy. See my layerlab project on github for an example of the latter.

@YannickJadoul
Copy link
Collaborator Author

I have just found out that Boost.Python doesn't support assigning a Subclass* to Superclass* either, actually.

void a_by_val(A  a) { std::cout << "Called a_by_val" << std::endl; }
void b_by_val(B  b) { std::cout << "Called b_by_val" << std::endl; }
void a_by_ref(A &a) { std::cout << "Called a_by_ref" << std::endl; }
void b_by_ref(B &b) { std::cout << "Called b_by_ref" << std::endl; }
void a_by_ptr(A *a) { std::cout << "Called a_by_ptr" << std::endl; }
void b_by_ptr(B *b) { std::cout << "Called b_by_ptr" << std::endl; }

B *create_b() { return new B(); }
...
    def("a_by_val", &a_by_val);
    def("b_by_val", &b_by_val);
    def("a_by_ref", &a_by_ref);
    def("b_by_ref", &b_by_ref);
    def("a_by_ptr", &a_by_ptr);
    def("b_by_ptr", &b_by_ptr);
    def("create_b", &create_b, return_value_policy<manage_new_object>());
...
>>> b_ptr = create_b()
>>> a_by_ptr(b_ptr)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Boost.Python.ArgumentError: Python argument types in
    test_boost.a_by_ptr(A)
did not match C++ signature:
    a_by_ptr(A*)
>>> a_by_ref(b_ptr)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Boost.Python.ArgumentError: Python argument types in
    test_boost.a_by_ref(A)
did not match C++ signature:
    a_by_ref(A {lvalue})
>>> a_by_val(b_ptr)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Boost.Python.ArgumentError: Python argument types in
    test_boost.a_by_val(A)
did not match C++ signature:
    a_by_val(A)

So, maybe the question is whether you'd want this behaviour in general, or not. If you do not want this in general, maybe there is an easy way of just forcing a member function to take an argument of the wrapped class?

@wjakob
Copy link
Member

wjakob commented Nov 16, 2015

ok, thanks for clarifying. I was myself wondering how boost.python could figure this out at runtime if one of the types is never explicitly referenced.

Generally you'll have to write a small wrapper function which replaces the type in question, as in

 .def("i", [](B &b) { b.i(); })

This function could be written manually in each case, or automatically using a template-based approach.

However, it is really beyond the scope of this library, so I'm closing the ticket.

@wjakob wjakob closed this as completed Nov 16, 2015
@YannickJadoul
Copy link
Collaborator Author

Ok, fair enough. There is a simple workaround, anyway. Thanks.

I did notice another minor thing, while looking at the code, though, in the generated signature doc-string:

    m.def("before", &f);
    py::class_<X>(m, "Y")
        .def(py::init<>())
    ;
    m.def("after", &f);

results in

    after(...)
        Signature : (Y) -> None

    before(...)
        Signature : (X) -> None

as the detail::descr d = cast_in::name(kw.data(), def.data()); in cpp_function forces the nested descr instances of the tuple to be evaluated to strings.

I suppose it would be very uncommon to change the name of a class ánd have a circular dependency between classes (or some other reason why you cannot move 'before' after the class). And even then, it's only the documentation.
But just so you know, then ...

@wjakob
Copy link
Member

wjakob commented Nov 17, 2015

This behavior is intentional. Often the type may be some C++ template (e.g. Eigen::Matrix<Float, ...>) that looks very alien in a docstring. The idea is that you bind the class at the beginning (before instantiating functions that use it) and give it a more sane name that will be used in the Python API (like MatrixXf)

@YannickJadoul
Copy link
Collaborator Author

Ok, yes, so even if you have a circular dependency between 2 classes and their methods, you could first bind the two classes, by calling defonly later?

class B;
class A { public: void do(B &b); }
class B { public: void do(A &b); }

py::class_<A> cX(m, "X");
py::class_<B> cY(m, "Y");

cX.def("do", &A::do);
cY.def("do", &B::do);

Right. Not completely dummy proof (apparently), but it is indeed a solution for the rare case you have something like this.

@wjakob
Copy link
Member

wjakob commented Nov 17, 2015

(yep, that is what I meant)

nbigaouette pushed a commit to nbigaouette/pybind11 that referenced this issue Jan 12, 2016
This should insure that both the PythonLibs and PythonInterp points to
the same python version.

Without this, the Python library and interpreter might not match version.
For example, if both Python 2.7 and 3.4 is installed, PythonLibs will
find /usr/lib/x86_64-linux-gnu/libpython3.4m.so while
PythonInterp will find /usr/bin/python2.7 (at least on Ubuntu 14.04).

When PythonLibs and PythonInterp don't point to the same Python version,
the examples will all fail:

$ cmake ..
-- The C compiler identification is GNU 4.8.4
-- The CXX compiler identification is GNU 4.8.4
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Setting build type to 'MinSizeRel' as none was specified.
-- Found PythonLibs: /usr/lib/x86_64-linux-gnu/libpython3.4m.so (found suitable version "3.4.3", minimum required is "2.7")
-- Found PythonInterp: /usr/bin/python2.7 (found suitable version "2.7.6", minimum required is "2.7")
-- Performing Test HAS_LTO_FLAG
-- Performing Test HAS_LTO_FLAG - Success
-- Configuring done
-- Generating done
-- Build files have been written to: /home/nbigaouette/pybind11/build
$ make test
Running tests...
Test project /home/nbigaouette/pybind11/build
      Start  1: example1
 1/12 Test  pybind#1: example1 .........................***Failed    0.02 sec
      Start  2: example2
 2/12 Test  pybind#2: example2 .........................***Failed    0.03 sec
      Start  3: example3
 3/12 Test  pybind#3: example3 .........................***Failed    0.02 sec
      Start  4: example4
 4/12 Test  pybind#4: example4 .........................***Failed    0.02 sec
      Start  5: example5
 5/12 Test  pybind#5: example5 .........................***Failed    0.02 sec
      Start  6: example6
 6/12 Test  pybind#6: example6 .........................***Failed    0.02 sec
      Start  7: example7
 7/12 Test  pybind#7: example7 .........................***Failed    0.02 sec
      Start  8: example8
 8/12 Test  pybind#8: example8 .........................***Failed    0.02 sec
      Start  9: example9
 9/12 Test  pybind#9: example9 .........................***Failed    0.02 sec
      Start 10: example10
10/12 Test pybind#10: example10 ........................***Failed    0.02 sec
      Start 11: example11
11/12 Test pybind#11: example11 ........................***Failed    0.03 sec
      Start 12: example12
12/12 Test pybind#12: example12 ........................***Failed    0.02 sec

0% tests passed, 12 tests failed out of 12

Total Test time (real) =   0.25 sec

The following tests FAILED:
          1 - example1 (Failed)
          2 - example2 (Failed)
          3 - example3 (Failed)
          4 - example4 (Failed)
          5 - example5 (Failed)
          6 - example6 (Failed)
          7 - example7 (Failed)
          8 - example8 (Failed)
          9 - example9 (Failed)
         10 - example10 (Failed)
         11 - example11 (Failed)
         12 - example12 (Failed)
Errors while running CTest
make: *** [test] Error 8


By adding the EXACT version to the find_package() calls, the version
discrepency is at least caught at the cmake call:

$ cmake ..
-- The C compiler identification is GNU 4.8.4
-- The CXX compiler identification is GNU 4.8.4
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Setting build type to 'MinSizeRel' as none was specified.
CMake Error at /usr/share/cmake-2.8/Modules/FindPackageHandleStandardArgs.cmake:108 (message):
  Could NOT find PythonLibs: Found unsuitable version "3.4.3", but required
  is exact version "2.7" (found /usr/lib/x86_64-linux-gnu/libpython3.4m.so)
Call Stack (most recent call first):
  /usr/share/cmake-2.8/Modules/FindPackageHandleStandardArgs.cmake:313 (_FPHSA_FAILURE_MESSAGE)
  /usr/share/cmake-2.8/Modules/FindPythonLibs.cmake:208 (FIND_PACKAGE_HANDLE_STANDARD_ARGS)
  CMakeLists.txt:27 (find_package)


-- Configuring incomplete, errors occurred!
See also "/home/nbigaouette/pybind11/build/CMakeFiles/CMakeOutput.log".
nbigaouette pushed a commit to nbigaouette/pybind11 that referenced this issue Jan 12, 2016
…ts to

the same python version.

Without this, the Python library and interpreter might not match version.
For example, if both Python 2.7 and 3.4 is installed, `PythonLibs` will
find /usr/lib/x86_64-linux-gnu/libpython3.4m.so while
`PythonInterp` will find /usr/bin/python2.7 (at least on Ubuntu 14.04).

When `PythonLibs` and `PythonInterp` don't point to the same Python version,
the examples will all fail:

```bash
$ cmake ..
-- The C compiler identification is GNU 4.8.4
-- The CXX compiler identification is GNU 4.8.4
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Setting build type to 'MinSizeRel' as none was specified.
-- Found PythonLibs: /usr/lib/x86_64-linux-gnu/libpython3.4m.so (found suitable version "3.4.3", minimum required is "2.7")
-- Found PythonInterp: /usr/bin/python2.7 (found suitable version "2.7.6", minimum required is "2.7")
-- Performing Test HAS_LTO_FLAG
-- Performing Test HAS_LTO_FLAG - Success
-- Configuring done
-- Generating done
-- Build files have been written to: /home/nbigaouette/pybind11/build
$ make test
Running tests...
Test project /home/nbigaouette/pybind11/build
      Start  1: example1
 1/12 Test  pybind#1: example1 .........................***Failed    0.02 sec
      Start  2: example2
 2/12 Test  pybind#2: example2 .........................***Failed    0.03 sec
      Start  3: example3
 3/12 Test  pybind#3: example3 .........................***Failed    0.02 sec
      Start  4: example4
 4/12 Test  pybind#4: example4 .........................***Failed    0.02 sec
      Start  5: example5
 5/12 Test  pybind#5: example5 .........................***Failed    0.02 sec
      Start  6: example6
 6/12 Test  pybind#6: example6 .........................***Failed    0.02 sec
      Start  7: example7
 7/12 Test  pybind#7: example7 .........................***Failed    0.02 sec
      Start  8: example8
 8/12 Test  pybind#8: example8 .........................***Failed    0.02 sec
      Start  9: example9
 9/12 Test  pybind#9: example9 .........................***Failed    0.02 sec
      Start 10: example10
10/12 Test pybind#10: example10 ........................***Failed    0.02 sec
      Start 11: example11
11/12 Test pybind#11: example11 ........................***Failed    0.03 sec
      Start 12: example12
12/12 Test pybind#12: example12 ........................***Failed    0.02 sec

0% tests passed, 12 tests failed out of 12

Total Test time (real) =   0.25 sec

The following tests FAILED:
          1 - example1 (Failed)
          2 - example2 (Failed)
          3 - example3 (Failed)
          4 - example4 (Failed)
          5 - example5 (Failed)
          6 - example6 (Failed)
          7 - example7 (Failed)
          8 - example8 (Failed)
          9 - example9 (Failed)
         10 - example10 (Failed)
         11 - example11 (Failed)
         12 - example12 (Failed)
Errors while running CTest
make: *** [test] Error 8
```

By adding the `EXACT` version to the `find_package()` calls, the version
discrepancy is at least caught at the cmake call:

```bash
$ cmake ..
-- The C compiler identification is GNU 4.8.4
-- The CXX compiler identification is GNU 4.8.4
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Setting build type to 'MinSizeRel' as none was specified.
CMake Error at /usr/share/cmake-2.8/Modules/FindPackageHandleStandardArgs.cmake:108 (message):
  Could NOT find PythonLibs: Found unsuitable version "3.4.3", but required
  is exact version "2.7" (found /usr/lib/x86_64-linux-gnu/libpython3.4m.so)
Call Stack (most recent call first):
  /usr/share/cmake-2.8/Modules/FindPackageHandleStandardArgs.cmake:313 (_FPHSA_FAILURE_MESSAGE)
  /usr/share/cmake-2.8/Modules/FindPythonLibs.cmake:208 (FIND_PACKAGE_HANDLE_STANDARD_ARGS)
  CMakeLists.txt:27 (find_package)


-- Configuring incomplete, errors occurred!
See also "/home/nbigaouette/pybind11/build/CMakeFiles/CMakeOutput.log".
```
nbigaouette pushed a commit to nbigaouette/pybind11 that referenced this issue Jan 12, 2016
This should insure that both the `PythonLibs` and `PythonInterp` points to
the same python version.

Without this, the Python library and interpreter might not match version.
For example, if both Python 2.7 and 3.4 is installed, `PythonLibs` will
find /usr/lib/x86_64-linux-gnu/libpython3.4m.so while
`PythonInterp` will find /usr/bin/python2.7 (at least on Ubuntu 14.04).

When `PythonLibs` and `PythonInterp` don't point to the same Python version,
the examples will all fail:

```bash
$ cmake ..
-- The C compiler identification is GNU 4.8.4
-- The CXX compiler identification is GNU 4.8.4
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Setting build type to 'MinSizeRel' as none was specified.
-- Found PythonLibs: /usr/lib/x86_64-linux-gnu/libpython3.4m.so (found suitable version "3.4.3", minimum required is "2.7")
-- Found PythonInterp: /usr/bin/python2.7 (found suitable version "2.7.6", minimum required is "2.7")
-- Performing Test HAS_LTO_FLAG
-- Performing Test HAS_LTO_FLAG - Success
-- Configuring done
-- Generating done
-- Build files have been written to: /home/nbigaouette/pybind11/build
$ make test
Running tests...
Test project /home/nbigaouette/pybind11/build
      Start  1: example1
 1/12 Test  pybind#1: example1 .........................***Failed    0.02 sec
      Start  2: example2
 2/12 Test  pybind#2: example2 .........................***Failed    0.03 sec
      Start  3: example3
 3/12 Test  pybind#3: example3 .........................***Failed    0.02 sec
      Start  4: example4
 4/12 Test  pybind#4: example4 .........................***Failed    0.02 sec
      Start  5: example5
 5/12 Test  pybind#5: example5 .........................***Failed    0.02 sec
      Start  6: example6
 6/12 Test  pybind#6: example6 .........................***Failed    0.02 sec
      Start  7: example7
 7/12 Test  pybind#7: example7 .........................***Failed    0.02 sec
      Start  8: example8
 8/12 Test  pybind#8: example8 .........................***Failed    0.02 sec
      Start  9: example9
 9/12 Test  pybind#9: example9 .........................***Failed    0.02 sec
      Start 10: example10
10/12 Test pybind#10: example10 ........................***Failed    0.02 sec
      Start 11: example11
11/12 Test pybind#11: example11 ........................***Failed    0.03 sec
      Start 12: example12
12/12 Test pybind#12: example12 ........................***Failed    0.02 sec

0% tests passed, 12 tests failed out of 12

Total Test time (real) =   0.25 sec

The following tests FAILED:
          1 - example1 (Failed)
          2 - example2 (Failed)
          3 - example3 (Failed)
          4 - example4 (Failed)
          5 - example5 (Failed)
          6 - example6 (Failed)
          7 - example7 (Failed)
          8 - example8 (Failed)
          9 - example9 (Failed)
         10 - example10 (Failed)
         11 - example11 (Failed)
         12 - example12 (Failed)
Errors while running CTest
make: *** [test] Error 8
```

By adding the `EXACT` version to the `find_package()` calls, the version
discrepancy is at least caught at the cmake call:

```bash
$ cmake ..
-- The C compiler identification is GNU 4.8.4
-- The CXX compiler identification is GNU 4.8.4
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Setting build type to 'MinSizeRel' as none was specified.
CMake Error at /usr/share/cmake-2.8/Modules/FindPackageHandleStandardArgs.cmake:108 (message):
  Could NOT find PythonLibs: Found unsuitable version "3.4.3", but required
  is exact version "2.7" (found /usr/lib/x86_64-linux-gnu/libpython3.4m.so)
Call Stack (most recent call first):
  /usr/share/cmake-2.8/Modules/FindPackageHandleStandardArgs.cmake:313 (_FPHSA_FAILURE_MESSAGE)
  /usr/share/cmake-2.8/Modules/FindPythonLibs.cmake:208 (FIND_PACKAGE_HANDLE_STANDARD_ARGS)
  CMakeLists.txt:27 (find_package)


-- Configuring incomplete, errors occurred!
See also "/home/nbigaouette/pybind11/build/CMakeFiles/CMakeOutput.log".
```
EricCousineau-TRI referenced this issue in EricCousineau-TRI/pybind11 Feb 23, 2018
…_error

unique_ptr: Use original type caster, otherwise, will get faulty overloads!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants