pybind / pybind11

Seamless operability between C++11 and Python
https://pybind11.readthedocs.io/
Other
15.66k stars 2.1k forks source link

noexcept base class methods are not recognized in C++17 mode #2234

Open sizmailov opened 4 years ago

sizmailov commented 4 years ago

Issue description

Base class methods attributed with noexcept are not recognized by pybind11 overload system in C++17 mode. The example below compiles just fine in C++11/C++14 mode. Probably it's related to:

The noexcept-specification is a part of the function type and may appear as part of any function declarator. (since C++17)

https://en.cppreference.com/w/cpp/language/noexcept_spec#Explanation

Reproducible example code

#include "pybind11/pybind11.h"
#include "pybind11/embed.h"

namespace py = pybind11;

struct Base {
    int size() const noexcept { return 0; }
};

struct Derived : Base {};

PYBIND11_EMBEDDED_MODULE(example, m) {
  py::class_<Derived>(m, "Derived")
      .def(py::init<>())
      .def("size", &Derived::size);
}

int main() {
  py::scoped_interpreter guard{};
  py::exec(R"(
from example import Derived
d = Derived()
print(d.size())
)");
}

Output (C++17):

terminate called after throwing an instance of 'pybind11::error_already_set'
  what():  TypeError: size(): incompatible function arguments. The following argument types are supported:
    1. (self: Base) -> int

Invoked with: <example.Derived object at 0x7f42fb179ce0>

At:
  <string>(5): <module>

(tried with g++-8 / clang-9)

bstaletic commented 4 years ago

Python/Pybind11 doesn't know that Base is a base class of Derived. You can make this work like so:

#include "pybind11/pybind11.h"
#include "pybind11/embed.h"

namespace py = pybind11;

struct Base {
    int size() const noexcept { return 0; }
};

struct Derived : Base {};

PYBIND11_EMBEDDED_MODULE(example, m) {
  py::class_<Base>(m, ""); // Bind the base class
  py::class_<Derived, Base>(m, "Derived") // Tell python about the inheritance hierarchy
      .def(py::init<>())
      .def("size", &Derived::size);
}

int main() {
  py::scoped_interpreter guard{};
  py::exec(R"(
from example import Derived
d = Derived()
print(d.size())
)");
}
sizmailov commented 4 years ago

Sorry for unclear description. In my example I don't want to expose Base class to python. The snippet works fine without noexcept specifier, so I still think it's a bug.

wjakob commented 4 years ago

We recently added a special case for ref-qualified functions (https://github.com/pybind/pybind11/commit/63df87fa490d49244b76249854559ec8db22f119), now this seems like yet another annotation that would need to be supported. Not super-happy about the potential combinatorial explosion here, it would be nice if C++ would let us template over such things..

bstaletic commented 4 years ago

it would be nice if C++ would let us template over such things..

Did someone say Reflections? ...

Skylion007 commented 1 year ago

I think it might actually be possible to template over such things (at least in C++17 which is the only place it is necessary) by abusing/reimplementing std::invoke: https://stackoverflow.com/questions/60852108/overload-regardless-noexcept-specification (C++11 backport available here): https://web.archive.org/web/20150811205403/https://github.com/tomaszkam/proposals/blob/master/invoke/invoke_cpp11.hpp Stumped on how to actually implement this though, we may need to use std::invoke_result

Skylion007 commented 1 year ago

Okay, there is definitely a way around templating this, but I am bit lost on how to refactor this to proceed. We just need to use std::mem_fn and std::invoke (backported) to acomplish this. The C+11 backport could look something like this:

#if defined(PYBIND11_CPP17)
using std::invoke;
#else
    template<typename Functor, typename Object, typename... Args>
    constexpr auto invoke(Functor&& functor, Object&& object, Args&&... args)
      ->  typename std::enable_if<
            std::is_member_function_pointer<
              typename std::decay<Functor>::type
            >::value &&
            type_traits::is_target_reference<
              Object&&,
              typename std::decay<Functor>::type
            >::value,
            decltype((object.*functor)(std::forward<Args>(args)...))
          >::type
    {
      return (object.*functor)(std::forward<Args>(args)...);
    }

    template<typename Functor, typename Object, typename... Args>
    constexpr auto invoke(Functor&& functor, Object&& object, Args&&... args)
      ->  typename std::enable_if<
            std::is_member_function_pointer<
              typename std::decay<Functor>::type
            >::value &&
            !type_traits::is_target_reference<
              Object&&,
              typename std::decay<Functor>::type
            >::value,
            decltype(((*std::forward<Object>(object)).*functor)(std::forward<Args>(args)...))
          >::type
    {
      return ((*std::forward<Object>(object)).*functor)(std::forward<Args>(args)...);
    }

    template<typename Functor, typename Object>
    constexpr auto invoke(Functor&& functor, Object&& object)
      ->  typename std::enable_if<
            std::is_member_object_pointer<
              typename std::decay<Functor>::type
            >::value &&
            type_traits::is_target_reference<
              Object&&,
              typename std::decay<Functor>::type
            >::value,
            decltype(object.*functor)
          >::type
    {
      return object.*functor;
    }

    template<typename Functor, typename Object>
    constexpr auto invoke(Functor&& functor, Object&& object)
      ->  typename std::enable_if<
            std::is_member_object_pointer<
              typename std::decay<Functor>::type
            >::value &&
            !type_traits::is_target_reference<
              Object&&,
              typename std::decay<Functor>::type
            >::value,
            decltype((*std::forward<Object>(object)).*functor)
          >::type
    {
      return (*std::forward<Object>(object)).*functor;
    }

    template<typename Functor, typename... Args>
    constexpr auto invoke(Functor&& functor, Args&&... args)
      ->  typename std::enable_if<
            !std::is_member_pointer<
              typename std::decay<Functor>::type
            >::value,
            decltype(std::forward<Functor>(functor)(std::forward<Args>(args)...))
          >::type
    {
      return std::forward<Functor>(functor)(std::forward<Args>(args)...);
    }
#endif
Skylion007 commented 1 year ago

This template is horrific, but may be the easiest way to use downstream as it just converts noexcept functions types to none-noexcept function types. https://stackoverflow.com/a/55701361/2444240 Would still need to refactor the initialize overloads to use std::mem_fn though which seems difficult.

Skylion007 commented 1 year ago

Places that need updating are listed in this comment: https://github.com/pybind/pybind11/issues/2856#issuecomment-785020501

mayaknife commented 5 months ago

I'm running into this problem. I have a base class, which uses noexcept and which I cannot change, that pybind11 fails to find. Is there a workaround that I can use in the derived class to get around this?