Wrapping function templates (or overload sets) into Python can be a real challenge. Python doesn’t let you define functions with the same name but different argument types like C++ does.

Consider this pair of functions that calculate the magnitude of a scalar or complex number:

double mag(double v)
{
return std::fabs(v);  // from <cmath>
}

double mag(std::complex<double> v)
{
return std::abs(v);   // the overload in <complex>
}



In C++ we use the same name, mag, to refer to either of these and the compiler selects the right one. We’d like it to work the same way when bound into Python:

from cppmodule import mag

if __name__ == "__main__":
print(mag(3+4j))     # complex
print(mag(-3.14))    # scalar double



which should print 5.0 and 3.14.

## Two Common Approaches

In my experience looking at Python libraries I’ve seen two popular strategies that give this result:

### Use value wrapper, bind one function, dispatch in C++

In this approach we use some kind of type-erased wrapper, like std::any, as a container type for whatever was actually supplied. The bound function then does type checking on the contained value and dispatches to the correct concrete function. For example:

double mag(std::any v)
{
// dispatch in C++ from this single function

auto * d = std::any_cast<double>(&v);  // nullptr if not double
if (d)
{
return std::fabs(*d);
} else {
auto c = std::any_cast<std::complex<double>>(v);
return std::abs(c);
}

}


Here I’ve also let Boost.Python know that our argument types are convertible to std::any:

BOOST_PYTHON_MODULE(strategy1) {
using namespace boost::python;

implicitly_convertible<double, std::any>();
implicitly_convertible<std::complex<double>, std::any>();

def("mag", mag);
}


### Bind N functions, dispatch in Python

In this alternative we register all the function variations, then let a Python wrapper function dispatch according to what it receives:

from cppmodule import magScalar, magComplex

def mag(v):
# dispatch from Python to per-type C++ functions
if type(v) is complex:
return magComplex(v)
else:
return magScalar(v)


I find both approaches unsatisfying. Iterating over types at runtime is inefficient and error-prone. Plus, we already have a way to express “one of these N types” in C++ : std::variant.

## Using std::variant with Python

My goal is to directly express the types my functions can accept directly in the interface, and bind each function just once. Then I can flexibly and efficiently dispatch with std::visit. For example, using the overload set from the first alternative:

double magImpl(std::variant<double, std::complex<double>> var)
{
// compare vs. std::any_cast dispatcher, above
return std::visit([](auto const & v) -> double
{
return mag(v);  // don't need to list out each type
},
var);
}


The visitor gets instantiated once per member type so I could also consolidate the overloads with if constexpr :

double mag(std::variant<double, std::complex<double>> var)
{
return std::visit([](auto const & v) -> double
{
if constexpr (std::is_same_v<decltype(v), double>)
return std::fabs(v);  // <cmath>
else
return std::abs(v);   // <complex>
},
var);
}



## Implementation

### Goals

With a minimum of boilerplate:

1. std::variant<X, Y, Z> parameters, when bound into Python, accept any of X, Y, Z if they are themselves convertible.
2. Supplying an argument not convertible into one of the member types throws an exception
3. std::monostate, if a member type, allows the Python value None to be supplied
4. std::reference_wrapper should be supported without copying the underlying type
5. Return values should be supported similarly

### Details

Using Boost.Python to make this happen was not too painful, though I had to explore the code to understand how to do it. We need to register special conversion functions for:

1. Constructing a variant from one of its alternative types (for arguments)
2. Producing Python objects from a variant (for return values)

The good news is I can assume both functions exist for each variant’s constituent types, so a kind of “recursive” implementation follows.

### Variant Arguments

The thing that almost works is to notify Boost.Python that the member types are implicitly convertible to the variant itself. Something like this:

// assuming we have std::variant<T...> here:
mp11::mp_for_each<         // a little help from Boost.MP11
mp11::mp_list<T...>>(
[](auto t){
using arg_t = std::decay_t<decltype(t)>;

// take advantage of built-in conversions
python::implicitly_convertible<val_t, var_t>();
});



This breaks down in two unfortunate ways:

1. Some alternative types may not be default-constructible.
2. The implicit conversion process is more lax about numbers than I would like, and picks the first match in the variant, so you get e.g. int instead of bool when you try to store True into a std::variant<int, bool>.

The solution to the first problem is just to iterate over pointer types instead. The second required implementing my own “converter” for numeric types, but it wasn’t bad.

#### Reference Wrappers

Supporting std::reference_wrapper<T> just required detecting and using the underlying type instead.

### Variant Return Types

This one’s quite straightforward - you just dispatch on the stored type with std::visit and convert it naturally:

static PyObject* convert(std::variant<T...> const & var)
{
// convert variant by recursively visiting stored types T...
return std::visit(
[](auto const & v){
// convert this type
return boost::python::to_python_value<decltype(v)>()(v);
},
var);
}


### The Code

A repo demonstrating this approach, along with all the examples in this post, can be found here. The key implementation code is here

## A More Interesting Example

Real applications might have multiple parameters, each of varying types, and produce variant results as well. Here’s a relatively simple one:

Imagine a wrapped C++ function adder that accepts two arguments, each of either integer or string type, and returns:

1. The concatenation of the two inputs, if either input is a string
2. The sum of the two inputs, if both are integers

Usage might look like this:

>>> print(adder("the answer is ", 42))  # string and int
>>> print(adder("a monoid", " in the category of endofunctors")) # two strings
a monoid in the category of endofunctors
>>> print(adder(1, 2))                  # two ints
3
>>> print(type(adder(1, 2)))            # not a string this time
<class 'int'>
>>> print(adder(2, 1.14))               # should fail with type error
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Boost.Python.ArgumentError: Python argument types in
did not match C++ signature:
...


And the C++ implementation could be:

using var_t = std::variant<std::string, int>;

var_t adder(var_t const& var_a, var_t const& var_b)
{
return std::visit(
[](auto const & a, auto const & b) -> var_t
{
// return integer sum if both are ints, otherwise a string append
if constexpr (std::is_same_v<decltype(a), decltype(b)>)
{
return a + b;          // works for both types
} else {
if constexpr (std::is_same_v<std::decay_t<decltype(a)>,
std::string>)
return a + std::to_string(b);
else
return std::to_string(a) + b;
}
},
var_a,
var_b);
}


## Conclusion

std::variant is a natural way to express “one of these types” and is a good fit for wrapped Python function parameters. Clean, efficient dispatching is built right into C++17 with std::visit, and type checking happens automatically.

## References

• A StackOverflow user applied a similar philosophy, but for SWIG, here.
• PyBind11 seems to have built-in support for variant parameters