Read Part 1 - C++ function decorators here
In this second experiment, I re-imagine the way class member functions can be decorated. In the first article, I set out to achieve a close equivalent of python function decorators without the use of magic MACROS or mocs. Using purely C++ 14 we came up with a design pattern to accept arbitrary function inputs that return a closure function, allowing programmers to aggregate functions together and in compile-time!
How to write class member decorator functions in modern C++14 or higher
Works 100% on MSVC, Clang, and GNU CC compilers
decorated member functor with private class implementation
dynamic member functor re-assignment
We left off with a demonstration that class member functions could also be decorated but I wasn't satisfied with the syntax. To refresh, we left with something that looked like this:
// Different prices for different apples
apples groceries1(1.09), groceries2(3.0), groceries3(4.0);
auto get_cost = log_time(output(exception_fail_safe(visit_apples(&apples::calculate_cost))));
auto vec = {
get_cost(groceries2, 2, 1.1),
get_cost(groceries3, 5, 1.3),
get_cost(groceries1, 4, 0)
};
Which is ugly and inconvenient if we want to pass objects around our system, we would need to have access to these decorators in each file and rewrite our code to use these decorators instead of the object's own methods.
In the first article, I pointed out that python member functions could be reassigned on the fly and decorated which is impossible in vanilla C++. There may be times when I want to log a function in one particular location instead of seeing logs every time the function is called. With dynamic reassignment, I could decorate the function on the fly, invoke it, and set it back to the original implementation afterwards.
If there was only a way these python features could make their way into C++, I'd be a happy guy. I like to have my cake and eat it too, don't you? 🍰
By the end of the first article, we needed a way for our decorators to expect a class object in order to invoke a class member function. We wrote it to visit apples
class objects and it looked like this:
template<typename F>
auto visit_apples(F func) {
return [func](apples& a, auto... args) {
return (a.*func)(args...);
};
}
In python, we have a similar decorator to properly decorate member functions: @classmethod
. This decorator specifically tells the interpreter to pass self
into the decorator chain, if used, so that the member function can be called correctly- specifically in the event of inherited member functions. Further reading on stackoverflow
We've done something similar earlier, we needed to pass the instance of the object into the decorator chain and with a quick re-write we can make this class visitor, universal.
Simply swap out apples&
for auto&
:
////////////////////////////////////
// visitor function //
////////////////////////////////////
template<typename F>
constexpr auto classmethod(F func) {
return [func](auto& a, auto&&... args) {
return (a.*func)(args...);
};
}
We need to store a lambda. The C++ compiler does a good job hiding the implementation of lambdas, it's hard to think of them as anything else but magical.
Lambdas are functors: a class object that has an overloaded operator()
to behave like a function. The C++ standard already has a function utility header <functional>
which provides a wrapper around any functor we throw at it.
Initially we could write
std::function<double(int, double)> f = get_cost;
But then we've not made any room to pass the class object for the classmethod
decorator.
We could re-write it a little
std::function<double(apples&, int, double)> f = get_cost;
and further generalize it by rewritting it as a class
template<typename ClassType>
class class_methodfunc {
std::function<double(ClassType&, int, double)> f;
// ...
};
We need a way to allow our functor to accept any arbitrary input - just like our initial decorator problem from before. Unlike last time, we need to know the exact type information at compile-time in order to store these decorators. Let's generalize further.
template<typename ClassType, typename RType, typename... Args>
class class_memberfunc {
std::function<RType(ClassType&, Args...)> f;
// to accept any functor including lambdas and deeply-nested decorators, we use a single typename
template<typename F>
void operator=(const F& rhs) {
f = std::function<RType(ClassType&, Args...)>(rhs);
}
RType operator()(ClassType& self, Args&&... args) {
return f(self, args...);
}
};
Let's start by using our new class_memberfunc
to rewrite the apples
class and provide a private member function that we want to decorate. Remember, we cannot reassign or modify a C++ function once it's written but we can reassign a functor object.
First, let's add an implicit type conversion in our rusty optional_type<>
class:
template<typename T>
struct optional_type {
T value;
bool OK;
bool BAD;
std::string msg;
// implicitly dissolve into value type T
operator T() {
return value;
}
optional_type(T t) : value(t) { OK = true; BAD = false; }
optional_type(bool ok, std::string msg="") : msg(msg) { OK = ok; BAD = !ok; }
};
If we try to decorate member functions that do not return optional values, we will get compile errors.
class apples {
private:
// private member function implementation that throws
double calculate_cost_impl(int count, double weight) {
if(count <= 0)
throw std::runtime_error("must have 1 or more apples");
if(weight <= 0)
throw std::runtime_error("apples must weigh more than 0 ounces");
return count*weight*cost_per_apple;
}
double cost_per_apple;
public:
// ctor
apples(double cost_per_apple) :
cost_per_apple(cost_per_apple) {
// decorate our member function in ctor and store result in functor
this->calculate_cost = log_time(output(exception_fail_safe(classmethod(&apples::calculate_cost_impl))));
}
~apples() { }
// define a functor with the same signature as our member function
class_memberfunc<apples, optional_type<double>, int, double> calculate_cost;
};
We're getting there but we've come full circle with needing to provide the class object when we invoke the function with operator()
:
groceries1.calculate_cost(groveries1, 5, 6.1);
That's not very native at all. And we need to specify the return type and argument list by hand.
Let's inspect the contents of the function signatures that we expect our member functor to take. We want to achieve the following syntax somehow:
// define a functor with the same signature as our member function
memberfunc<double(int, double)> calculate_cost;
...
groceries1.calculate_cost(5, 6.1); // implicit class object used! But how?
First, we need to somehow pull the argument list out of the function sig as well as the return type. We can create a templated structure that stores these types publically so we can query them at compile time:
///////////////////////////////////////////////
// function traits //
///////////////////////////////////////////////
template<typename T>
struct function_traits;
// traits allows us to inspect type information from our function signature
template<typename R, typename Class, typename... Args>
struct function_traits<R (Class::*)(Args...)>
{
typedef R result_type;
using args_pack = std::tuple<Args...>;
};
template<typename R, typename... Args>
struct function_traits<R(Args...)>
{
typedef R result_type;
using args_pack = std::tuple<Args...>;
};
This is a simple function-trait utlity structure. All we care about is the ability to deduce the return type and argument list from any kind of callable. Now we can write an alias to deduce the correct class_memberfunction
signature.
class apples {
/*
define our member functor alias
use function traits to deduce types
*/
template<typename Type>
using memberfunc = class_memberfunc<
apples,
typename function_traits<Type>::result_type,
typename function_traits<Type>::args_pack
>;
With this short-hand we don't need to type out everything explicitly.
// define a functor with the same signature as our member function
memberfunc<double(int, double)> calculate_cost;
We can also let the compiler figure out our signature for us. This way we only modify one place in the code and the rest will follow.
memberfunc<decltype(&apples::calculate_cost_impl)> calculate_cost;
Handy!
Python implicity passes along the instance object of the class using the special keyword self
. Let's pass in the C++ equivalent this
in the constructor of our member functor and pass that along to the functor's operator()
!
// ctor
apples(double cost_per_apple) :
calculate_cost(this),
cost_per_apple(cost_per_apple) {
// decorate our member function in ctor
this->calculate_cost = log_time(output(exception_fail_safe(classmethod(&apples::calculate_cost_impl))));
}
Now we're getting somewhere!
groceries1.calculate_cost(5, 6.1);
output is
Bag cost $18.203
> Logged at Sun Aug 18 17:01:32 2019
I'm all about typing less. As I get older, while I appreicate the verbose descriptors of Java functions and classes, carpel tunnel is right around the corner waiting to strike.
Let's hide the ugly alias in an enabler trait class and inherit from it instead
template<typename Class>
class enable_memberfunc_traits {
protected:
/*
define our member functor alias
use function traits to deduce types
*/
template<typename Type>
using memberfunc = class_memberfunc<Class,
typename function_traits<Type>::result_type,
typename function_traits<Type>::args_pack>;
};
class apples : public enable_memberfunc_traits<apples> {
// ... omitted ...
memberfunc<decltype(&apples::calculate_cost_impl)> calculate_cost;
};
We did it! We have a private implementation we wanted to decorate, and it acts on this
just like a member function! We wrote a classmethod
universal visitor decorator and it's shaping up to be very flexible like Python. We've accomplished every goal for the member functors that we set out to achieve. There is one more tidbit I'd like to accomplish: in the previous article, Python allows us to assign a function to a decorated version of itself e.g.
class Foo:
def add(self, value):
self.value = self.value + value
def stars(func):
def inner(*args, **kwargs):
print("**********")
func(*args, **kwargs)
print("**********")
return inner
Foo.add = stars(Foo.add)
The assignment Foo.add = stars(Foo.add)
was impossible before but we now have a powerful member functor class. Let's add the ability to convert our member functor back into it's true lambda form by returning the inner f
functor object:
template<typename ClassType, typename RType, typename... Args>
class class_memberfunc<ClassType, RType, std::tuple<Args...>> {
// ... omitted ....
std::function<RType(ClassType&, Args...)> operator*() {
return f;
}
// ...
};
Now, in order to dynamically decorate the private calculate_cost_impl
implementation, we must make it public. This may shake some heads and it's worth noting that in Python, everything is public. There is no such thing as a private function or member variable in Python.
Our crafty member functor can point to a class member function and be used as-is even without using the classmethod
decorator
See line 206
// in apples ctor
this->calculate_cost = &apples::calculate_cost_impl;
Now we can dynamically decorate the member function. Plus we can assign a function to a decorated version of itself.
Take a look at line 226 - 232
groceries1.calculate_cost = exception_fail_safe(classmethod(&apples::calculate_cost_impl));
// use star operator to return inner functor and re-assign
groceries1.calculate_cost = log_time(output(*groceries1.calculate_cost));
std::cout << std::endl;
groceries1.calculate_cost(5, 3.34);
output is
18.203
Bag cost $18.203
> Logged at Sun Aug 18 18:00:15 2019
Thank you for reading