diff --git a/docs/advanced/classes.rst b/docs/advanced/classes.rst
index 569102efc..d578a1625 100644
--- a/docs/advanced/classes.rst
+++ b/docs/advanced/classes.rst
@@ -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__``.
@@ -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_(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_(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_(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(&Publicist::foo));``
+ where ``int (A::*)() const`` is the type of ``A::foo``.
diff --git a/tests/test_class.cpp b/tests/test_class.cpp
index 670949f80..af7b0bfb0 100644
--- a/tests/test_class.cpp
+++ b/tests/test_class.cpp
@@ -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(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_(m, "ProtectedA")
+ .def(py::init<>())
+#if !defined(_MSC_VER) || _MSC_VER >= 1910
+ .def("foo", &PublicistA::foo);
+#else
+ .def("foo", static_cast(&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_(m, "ProtectedB")
+ .def(py::init<>())
+#if !defined(_MSC_VER) || _MSC_VER >= 1910
+ .def("foo", &PublicistB::foo);
+#else
+ .def("foo", static_cast(&PublicistB::foo));
+#endif
}
template class BreaksBase { public: virtual ~BreaksBase() = default; };
diff --git a/tests/test_class.py b/tests/test_class.py
index 9c5049f72..c8ff85735 100644
--- a/tests/test_class.py
+++ b/tests/test_class.py
@@ -176,3 +176,22 @@ def test_operator_new_delete(capture):
"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