Skip to content

Test and document binding protected member functions #1013

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

Merged
merged 1 commit into from
Aug 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 76 additions & 1 deletion docs/advanced/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ functions, and :func:`PYBIND11_OVERLOAD` should be used for functions which have
a default implementation. There are also two alternate macros
:func:`PYBIND11_OVERLOAD_PURE_NAME` and :func:`PYBIND11_OVERLOAD_NAME` which
take a string-valued name argument between the *Parent class* and *Name of the
function* slots, which defines the name of function in Python. This is required
function* slots, which defines the name of function in Python. This is required
when the C++ and Python versions of the
function have different names, e.g. ``operator()`` vs ``__call__``.

Expand Down Expand Up @@ -916,3 +916,78 @@ Python type created elsewhere.

The file :file:`tests/test_local_bindings.cpp` contains additional examples
that demonstrate how ``py::module_local()`` works.

Binding protected member functions
==================================

It's normally not possible to expose ``protected`` member functions to Python:

.. code-block:: cpp

class A {
protected:
int foo() const { return 42; }
};

py::class_<A>(m, "A")
.def("foo", &A::foo); // error: 'foo' is a protected member of 'A'

On one hand, this is good because non-``public`` members aren't meant to be
accessed from the outside. But we may want to make use of ``protected``
functions in derived Python classes.

The following pattern makes this possible:

.. code-block:: cpp

class A {
protected:
int foo() const { return 42; }
};

class Publicist : public A { // helper type for exposing protected functions
public:
using A::foo; // inherited with different access modifier
};

py::class_<A>(m, "A") // bind the primary class
.def("foo", &Publicist::foo); // expose protected methods via the publicist

This works because ``&Publicist::foo`` is exactly the same function as
``&A::foo`` (same signature and address), just with a different access
modifier. The only purpose of the ``Publicist`` helper class is to make
the function name ``public``.

If the intent is to expose ``protected`` ``virtual`` functions which can be
overridden in Python, the publicist pattern can be combined with the previously
described trampoline:

.. code-block:: cpp

class A {
public:
virtual ~A() = default;

protected:
virtual int foo() const { return 42; }
};

class Trampoline : public A {
public:
int foo() const override { PYBIND11_OVERLOAD(int, A, foo, ); }
};

class Publicist : public A {
public:
using A::foo;
};

py::class_<A, Trampoline>(m, "A") // <-- `Trampoline` here
.def("foo", &Publicist::foo); // <-- `Publicist` here, not `Trampoline`!

.. note::

MSVC 2015 has a compiler bug (fixed in version 2017) which
requires a more explicit function binding in the form of
``.def("foo", static_cast<int (A::*)() const>(&Publicist::foo));``
where ``int (A::*)() const`` is the type of ``A::foo``.
51 changes: 51 additions & 0 deletions tests/test_class.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,57 @@ TEST_SUBMODULE(class_, m) {
// This test is actually part of test_local_bindings (test_duplicate_local), but we need a
// definition in a different compilation unit within the same module:
bind_local<LocalExternal, 17>(m, "LocalExternal", py::module_local());

// test_bind_protected_functions
class ProtectedA {
protected:
int foo() const { return value; }

private:
int value = 42;
};

class PublicistA : public ProtectedA {
public:
using ProtectedA::foo;
};

py::class_<ProtectedA>(m, "ProtectedA")
.def(py::init<>())
#if !defined(_MSC_VER) || _MSC_VER >= 1910
.def("foo", &PublicistA::foo);
#else
.def("foo", static_cast<int (ProtectedA::*)() const>(&PublicistA::foo));
#endif

class ProtectedB {
public:
virtual ~ProtectedB() = default;

protected:
virtual int foo() const { return value; }

private:
int value = 42;
};

class TrampolineB : public ProtectedB {
public:
int foo() const override { PYBIND11_OVERLOAD(int, ProtectedB, foo, ); }
};

class PublicistB : public ProtectedB {
public:
using ProtectedB::foo;
};

py::class_<ProtectedB, TrampolineB>(m, "ProtectedB")
.def(py::init<>())
#if !defined(_MSC_VER) || _MSC_VER >= 1910
.def("foo", &PublicistB::foo);
#else
.def("foo", static_cast<int (ProtectedB::*)() const>(&PublicistB::foo));
#endif
}

template <int N> class BreaksBase { public: virtual ~BreaksBase() = default; };
Expand Down
19 changes: 19 additions & 0 deletions tests/test_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,22 @@ class SubAliased(m.AliasedHasOpNewDelSize):
"C delete " + sz_noalias + "\n" +
"C delete " + sz_alias + "\n"
)


def test_bind_protected_functions():
"""Expose protected member functions to Python using a helper class"""
a = m.ProtectedA()
assert a.foo() == 42

b = m.ProtectedB()
assert b.foo() == 42

class C(m.ProtectedB):
def __init__(self):
m.ProtectedB.__init__(self)

def foo(self):
return 0

c = C()
assert c.foo() == 0